Skip to content

Commit 47680d5

Browse files
committed
Optional Bluetooth LE advertisement for BLE-MIDI
1 parent e35ddaf commit 47680d5

10 files changed

+315
-48
lines changed

doc/bluealsa.8.rst

+17-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ bluealsa
66
Bluetooth Audio ALSA Backend
77
----------------------------
88

9-
:Date: July 2023
9+
:Date: January 2024
1010
:Manual section: 8
1111
:Manual group: System Manager's Manual
1212
:Version: $VERSION$
@@ -20,7 +20,7 @@ DESCRIPTION
2020
===========
2121

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

@@ -246,6 +246,9 @@ OPTIONS
246246
- **standard** - standard quality (44.1 kHz: 606 kbps, 48 kHz: 660 kbps)
247247
- **high** - high quality (44.1 kHz: 909 kbps, 48 kHz: 990 kbps)
248248

249+
--midi-advertisement
250+
Advertise BLE-MIDI service using Bluetooth LE advertising.
251+
249252
--xapl-resp-name=NAME
250253
Set the product name send in the XAPL response message.
251254
By default, the name is set as "BlueALSA".
@@ -260,10 +263,12 @@ Profiles
260263
--------
261264

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

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

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

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

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

455-
Copyright (c) 2016-2023 Arkadiusz Bokowy.
462+
Copyright (c) 2016-2024 Arkadiusz Bokowy.
456463

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

src/bluealsa-config.h

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* BlueALSA - bluealsa-config.h
3-
* Copyright (c) 2016-2023 Arkadiusz Bokowy
3+
* Copyright (c) 2016-2024 Arkadiusz Bokowy
44
*
55
* This file is a part of bluez-alsa.
66
*
@@ -128,6 +128,13 @@ struct ba_config {
128128

129129
} a2dp;
130130

131+
#if ENABLE_MIDI
132+
struct {
133+
/* advertise BLE-MIDI via LE advertisement */
134+
bool advertise;
135+
} midi;
136+
#endif
137+
131138
/* BlueALSA supports 5 SBC qualities: low, medium, high, XQ and XQ+. The XQ
132139
* mode uses 44.1 kHz sampling rate, dual channel mode with bitpool 38, 16
133140
* blocks in frame, 8 frequency bands and allocation method Loudness, which

src/bluez-iface.h

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* BlueALSA - bluez-iface.h
3-
* Copyright (c) 2016-2023 Arkadiusz Bokowy
3+
* Copyright (c) 2016-2024 Arkadiusz Bokowy
44
*
55
* This file is a part of bluez-alsa.
66
*
@@ -69,6 +69,14 @@ OrgBluezGattService1Skeleton *org_bluez_gatt_service1_skeleton_new(
6969
const GDBusInterfaceSkeletonVTable *vtable, void *userdata,
7070
GDestroyNotify userdata_free_func);
7171

72+
typedef struct {
73+
GDBusInterfaceSkeletonEx parent;
74+
} OrgBluezLeadvertisement1Skeleton;
75+
76+
OrgBluezLeadvertisement1Skeleton *org_bluez_leadvertisement1_skeleton_new(
77+
const GDBusInterfaceSkeletonVTable *vtable, void *userdata,
78+
GDestroyNotify userdata_free_func);
79+
7280
#endif
7381

7482
typedef struct {

src/bluez-iface.xml

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!-- BlueALSA - bluez-iface.xml
2-
Copyright (c) 2016-2023 Arkadiusz Bokowy
2+
Copyright (c) 2016-2024 Arkadiusz Bokowy
33
44
This file is a part of bluez-alsa.
55
@@ -40,6 +40,15 @@
4040
<property name="Primary" type="b" access="read"/>
4141
</interface>
4242

43+
<interface name="org.bluez.LEAdvertisement1">
44+
<method name="Release">
45+
</method>
46+
<property name="Type" type="s" access="read"/>
47+
<property name="ServiceUUIDs" type="as" access="read"/>
48+
<property name="Discoverable" type="b" access="read"/>
49+
<property name="Includes" type="as" access="read"/>
50+
</interface>
51+
4352
<interface name="org.bluez.MediaEndpoint1">
4453
<method name="SelectConfiguration">
4554
<arg direction="in" type="ay" name="capabilities"/>

src/bluez-midi.c

+126-23
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
struct bluez_midi_app {
5151
/* D-Bus object registration paths */
5252
char path[64];
53+
char path_adv[64 + 8];
5354
char path_service[64 + 8];
5455
char path_char[64 + 16];
5556
/* associated adapter */
@@ -69,14 +70,6 @@ struct bluez_midi_app {
6970
* D-Bus unique name of the BlueZ daemon. */
7071
static char bluez_dbus_unique_name[64] = "";
7172

72-
static GVariant *variant_new_midi_service_uuid(void) {
73-
return g_variant_new_string(BT_UUID_MIDI);
74-
}
75-
76-
static GVariant *variant_new_midi_characteristic_uuid(void) {
77-
return g_variant_new_string(BT_UUID_MIDI_CHAR);
78-
}
79-
8073
static struct bluez_midi_app *bluez_midi_app_ref(struct bluez_midi_app *app) {
8174
atomic_fetch_add_explicit(&app->ref_count, 1, memory_order_relaxed);
8275
return app;
@@ -128,12 +121,70 @@ static struct ba_transport *bluez_midi_transport_new(
128121
return t;
129122
}
130123

124+
static void bluez_midi_advertisement_release(
125+
GDBusMethodInvocation *inv, void *userdata) {
126+
struct bluez_midi_app *app = userdata;
127+
debug("Releasing MIDI LE advertisement: %s", app->path);
128+
g_object_unref(inv);
129+
}
130+
131+
static GVariant *bluez_midi_advertisement_iface_get_property(
132+
const char *property, G_GNUC_UNUSED GError **error, void *userdata) {
133+
(void)userdata;
134+
135+
if (strcmp(property, "Type") == 0)
136+
return g_variant_new_string("peripheral");
137+
if (strcmp(property, "ServiceUUIDs") == 0) {
138+
static const char *uuids[] = { BT_UUID_MIDI };
139+
return g_variant_new_strv(uuids, ARRAYSIZE(uuids));
140+
}
141+
if (strcmp(property, "Discoverable") == 0)
142+
/* advertise as general discoverable LE-only device */
143+
return g_variant_new_boolean(TRUE);
144+
if (strcmp(property, "Includes") == 0) {
145+
const char *values[] = { "local-name" };
146+
return g_variant_new_strv(values, ARRAYSIZE(values));
147+
}
148+
149+
g_assert_not_reached();
150+
return NULL;
151+
}
152+
153+
static GDBusObjectSkeleton *bluez_midi_advertisement_skeleton_new(
154+
struct bluez_midi_app *app) {
155+
156+
static const GDBusMethodCallDispatcher dispatchers[] = {
157+
{ .method = "Release",
158+
.sender = bluez_dbus_unique_name,
159+
.handler = bluez_midi_advertisement_release },
160+
{ 0 },
161+
};
162+
163+
static const GDBusInterfaceSkeletonVTable vtable = {
164+
.dispatchers = dispatchers,
165+
.get_property = bluez_midi_advertisement_iface_get_property,
166+
};
167+
168+
OrgBluezLeadvertisement1Skeleton *ifs_gatt_adv;
169+
if ((ifs_gatt_adv = org_bluez_leadvertisement1_skeleton_new(&vtable,
170+
app, (GDestroyNotify)bluez_midi_app_unref)) == NULL)
171+
return NULL;
172+
173+
GDBusInterfaceSkeleton *ifs = G_DBUS_INTERFACE_SKELETON(ifs_gatt_adv);
174+
GDBusObjectSkeleton *skeleton = g_dbus_object_skeleton_new(app->path_adv);
175+
g_dbus_object_skeleton_add_interface(skeleton, ifs);
176+
g_object_unref(ifs_gatt_adv);
177+
178+
bluez_midi_app_ref(app);
179+
return skeleton;
180+
}
181+
131182
static GVariant *bluez_midi_service_iface_get_property(
132183
const char *property, G_GNUC_UNUSED GError **error,
133184
G_GNUC_UNUSED void *userdata) {
134185

135186
if (strcmp(property, "UUID") == 0)
136-
return variant_new_midi_service_uuid();
187+
return g_variant_new_string(BT_UUID_MIDI);
137188
if (strcmp(property, "Primary") == 0)
138189
return g_variant_new_boolean(TRUE);
139190

@@ -198,7 +249,7 @@ static void bluez_midi_characteristic_acquire_write(
198249
}
199250

200251
int fds[2];
201-
if (socketpair(AF_LOCAL, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, fds) == -1) {
252+
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, fds) == -1) {
202253
error("Couldn't create BLE-MIDI char write socket pair: %s", strerror(errno));
203254
goto fail;
204255
}
@@ -258,7 +309,7 @@ static void bluez_midi_characteristic_acquire_notify(
258309
}
259310

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

296347
if (strcmp(property, "UUID") == 0)
297-
return variant_new_midi_characteristic_uuid();
348+
return g_variant_new_string(BT_UUID_MIDI_CHAR);
298349
if (strcmp(property, "Service") == 0)
299350
return g_variant_new_object_path(app->path_service);
300351
if (strcmp(property, "WriteAcquired") == 0)
@@ -371,6 +422,59 @@ static void bluez_midi_app_register_finish(
371422

372423
}
373424

425+
static void bluez_midi_app_register(
426+
struct ba_adapter *adapter, struct bluez_midi_app *app) {
427+
428+
GDBusMessage *msg;
429+
msg = g_dbus_message_new_method_call(BLUEZ_SERVICE, adapter->bluez_dbus_path,
430+
BLUEZ_IFACE_GATT_MANAGER, "RegisterApplication");
431+
432+
g_dbus_message_set_body(msg, g_variant_new("(oa{sv})", app->path, NULL));
433+
434+
debug("Registering MIDI GATT application: %s", app->path);
435+
g_dbus_connection_send_message_with_reply(config.dbus, msg,
436+
G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL,
437+
bluez_midi_app_register_finish, NULL);
438+
439+
g_object_unref(msg);
440+
}
441+
442+
static void bluez_midi_app_advertise_finish(
443+
GObject *source, GAsyncResult *result, G_GNUC_UNUSED void *userdata) {
444+
445+
GError *err = NULL;
446+
GDBusMessage *rep;
447+
448+
if ((rep = g_dbus_connection_send_message_with_reply_finish(
449+
G_DBUS_CONNECTION(source), result, &err)) != NULL)
450+
g_dbus_message_to_gerror(rep, &err);
451+
452+
if (rep != NULL)
453+
g_object_unref(rep);
454+
if (err != NULL) {
455+
error("Couldn't advertise MIDI GATT application: %s", err->message);
456+
g_error_free(err);
457+
}
458+
459+
}
460+
461+
static void bluez_midi_app_advertise(
462+
struct ba_adapter *adapter, struct bluez_midi_app *app) {
463+
464+
GDBusMessage *msg;
465+
msg = g_dbus_message_new_method_call(BLUEZ_SERVICE, adapter->bluez_dbus_path,
466+
BLUEZ_IFACE_LE_ADVERTISING_MANAGER, "RegisterAdvertisement");
467+
468+
g_dbus_message_set_body(msg, g_variant_new("(oa{sv})", app->path_adv, NULL));
469+
470+
debug("Registering MIDI LE advertisement: %s", app->path);
471+
g_dbus_connection_send_message_with_reply(config.dbus, msg,
472+
G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL,
473+
bluez_midi_app_advertise_finish, NULL);
474+
475+
g_object_unref(msg);
476+
}
477+
374478
GDBusObjectManagerServer *bluez_midi_app_new(
375479
struct ba_adapter *adapter, const char *path) {
376480

@@ -379,6 +483,7 @@ GDBusObjectManagerServer *bluez_midi_app_new(
379483
return NULL;
380484

381485
snprintf(app->path, sizeof(app->path), "%s", path);
486+
snprintf(app->path_adv, sizeof(app->path_adv), "%s/adv", path);
382487
snprintf(app->path_service, sizeof(app->path_service), "%s/service", path);
383488
snprintf(app->path_char, sizeof(app->path_char), "%s/char", app->path_service);
384489
app->hci_dev_id = adapter->hci.dev_id;
@@ -404,19 +509,17 @@ GDBusObjectManagerServer *bluez_midi_app_new(
404509
g_dbus_object_manager_server_export(manager, skeleton);
405510
g_object_unref(skeleton);
406511

407-
g_dbus_object_manager_server_set_connection(manager, config.dbus);
408-
409-
GDBusMessage *msg;
410-
msg = g_dbus_message_new_method_call(BLUEZ_SERVICE, adapter->bluez_dbus_path,
411-
BLUEZ_IFACE_GATT_MANAGER, "RegisterApplication");
512+
if (config.midi.advertise) {
513+
skeleton = bluez_midi_advertisement_skeleton_new(app);
514+
g_dbus_object_manager_server_export(manager, skeleton);
515+
g_object_unref(skeleton);
516+
}
412517

413-
g_dbus_message_set_body(msg, g_variant_new("(oa{sv})", path, NULL));
518+
g_dbus_object_manager_server_set_connection(manager, config.dbus);
414519

415-
debug("Registering MIDI GATT application: %s", app->path);
416-
g_dbus_connection_send_message_with_reply(config.dbus, msg,
417-
G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL,
418-
bluez_midi_app_register_finish, NULL);
520+
bluez_midi_app_register(adapter, app);
521+
if (config.midi.advertise)
522+
bluez_midi_app_advertise(adapter, app);
419523

420-
g_object_unref(msg);
421524
return manager;
422525
}

src/main.c

+12
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ int main(int argc, char **argv) {
179179
#if ENABLE_MP3LAME
180180
{ "mp3-algorithm", required_argument, NULL, 12 },
181181
{ "mp3-vbr-quality", required_argument, NULL, 13 },
182+
#endif
183+
#if ENABLE_MIDI
184+
{ "midi-advertisement", no_argument, NULL, 22 },
182185
#endif
183186
{ "xapl-resp-name", required_argument, NULL, 16 },
184187
{ 0, 0, 0, 0 },
@@ -229,6 +232,9 @@ int main(int argc, char **argv) {
229232
#if ENABLE_MP3LAME
230233
" --mp3-algorithm=TYPE\t\tselect LAME encoder algorithm type\n"
231234
" --mp3-vbr-quality=MODE\tset LAME encoder VBR quality mode\n"
235+
#endif
236+
#if ENABLE_MIDI
237+
" --midi-advertisement\t\tenable LE advertisement for BLE-MIDI\n"
232238
#endif
233239
" --xapl-resp-name=NAME\t\tset product name used by XAPL\n"
234240
"\nAvailable BT profiles:\n"
@@ -532,6 +538,12 @@ int main(int argc, char **argv) {
532538
}
533539
#endif
534540

541+
#if ENABLE_MIDI
542+
case 22 /* --midi-advertisement */ :
543+
config.midi.advertise = true;
544+
break;
545+
#endif
546+
535547
case 16 /* --xapl-resp-name=NAME */ :
536548
config.hfp.xapl_product_name = optarg;
537549
break;

0 commit comments

Comments
 (0)