diff --git a/NEWS b/NEWS index b7883c070..037d51873 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,7 @@ unreleased ========== +- optional support for BLE MIDI 1.0 profile as a GATT server - command line option to set real-time priority for IO threads - allow to select individual extended controls in ALSA plug-in - codec-specific delay adjustment with ALSA control and persistency diff --git a/src/Makefile.am b/src/Makefile.am index 0d62fd59e..89fb2b07e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -85,7 +85,9 @@ endif if ENABLE_MIDI bluealsa_SOURCES += \ - bluez-midi.c + ble-midi.c \ + bluez-midi.c \ + midi.c endif if ENABLE_MPEG @@ -111,6 +113,7 @@ endif AM_CFLAGS = \ @AAC_CFLAGS@ \ + @ALSA_CFLAGS@ \ @APTX_CFLAGS@ \ @APTX_HD_CFLAGS@ \ @BLUEZ_CFLAGS@ \ @@ -127,6 +130,7 @@ AM_CFLAGS = \ LDADD = \ @AAC_LIBS@ \ + @ALSA_LIBS@ \ @APTX_HD_LIBS@ \ @APTX_LIBS@ \ @BLUEZ_LIBS@ \ diff --git a/src/ba-transport.c b/src/ba-transport.c index a76245f65..2173d0698 100644 --- a/src/ba-transport.c +++ b/src/ba-transport.c @@ -42,6 +42,7 @@ #include "bluez.h" #include "hci.h" #include "hfp.h" +#include "midi.h" #include "sco.h" #include "storage.h" #include "shared/a2dp-codecs.h" @@ -860,12 +861,13 @@ struct ba_transport *ba_transport_new_sco( #if ENABLE_MIDI static int transport_acquire_bt_midi(struct ba_transport *t) { - (void)t; - return 0; + return midi_transport_alsa_seq_create(t); } static int transport_release_bt_midi(struct ba_transport *t) { + midi_transport_alsa_seq_delete(t); + if (t->midi.ble_fd_write != -1) { debug("Releasing BLE-MIDI write link: %d", t->midi.ble_fd_write); close(t->midi.ble_fd_write); @@ -893,10 +895,28 @@ struct ba_transport *ba_transport_new_midi( t->profile = profile; + t->midi.seq_port = -1; + t->midi.ble_fd_write = -1; + t->midi.ble_fd_notify = -1; + + int err; + if ((err = snd_midi_event_new(1024, &t->midi.seq_parser)) < 0) { + error("Couldn't create MIDI event decoder: %s", snd_strerror(err)); + goto fail; + } + + /* Disable MIDI running status generated by the decoder. */ + snd_midi_event_no_status(t->midi.seq_parser, 1); + t->acquire = transport_acquire_bt_midi; t->release = transport_release_bt_midi; return t; + +fail: + ba_transport_unref(t); + errno = -err; + return NULL; } #endif @@ -1124,6 +1144,12 @@ void ba_transport_unref(struct ba_transport *t) { free(t->sco.ofono_dbus_path_modem); #endif } +#if ENABLE_MIDI + else if (t->profile & BA_TRANSPORT_PROFILE_MIDI) { + if (t->midi.seq_parser != NULL) + snd_midi_event_free(t->midi.seq_parser); + } +#endif #if DEBUG /* If IO threads are not terminated yet, we can not go any further. @@ -1316,6 +1342,11 @@ void ba_transport_set_codec( * errno is set to indicate the error. */ int ba_transport_start(struct ba_transport *t) { +#if ENABLE_MIDI + if (t->profile & BA_TRANSPORT_PROFILE_MIDI) + return midi_transport_start(t); +#endif + /* For A2DP Source profile only, it is possible that BlueZ will * activate the transport following a D-Bus "Acquire" request before the * client thread has completed the acquisition procedure by initializing @@ -1353,6 +1384,12 @@ int ba_transport_start(struct ba_transport *t) { * to call it from IO thread itself - it will cause deadlock! */ int ba_transport_stop(struct ba_transport *t) { +#if ENABLE_MIDI + if (t->profile & BA_TRANSPORT_PROFILE_MIDI) + // FIXME: Skip thread termination for MIDI profile. + midi_transport_stop(t); +#endif + if (ba_transport_thread_state_check_terminated(&t->thread_enc) && ba_transport_thread_state_check_terminated(&t->thread_dec)) return 0; diff --git a/src/ba-transport.h b/src/ba-transport.h index 7f02e5397..30f1090dc 100644 --- a/src/ba-transport.h +++ b/src/ba-transport.h @@ -22,9 +22,12 @@ #include #include +#include + #include "a2dp.h" #include "ba-device.h" #include "ba-transport-pcm.h" +#include "ble-midi.h" #include "bluez.h" #include "shared/a2dp-codecs.h" @@ -264,11 +267,29 @@ struct ba_transport { #if ENABLE_MIDI struct { + /* ALSA sequencer. */ + snd_seq_t *seq; + /* Associated sequencer port. */ + int seq_port; + + /* ALSA MIDI event parser. */ + snd_midi_event_t *seq_parser; + /* BLE-MIDI input link */ int ble_fd_write; /* BLE-MIDI output (notification) link */ int ble_fd_notify; + /* BLE-MIDI parser for the incoming data. */ + struct ble_midi_dec ble_decoder; + /* BLE-MIDI parser for the outgoing data. */ + struct ble_midi_enc ble_encoder; + + /* Watch ID associated with the BLE-MIDI link. */ + unsigned int watch_id_ble; + /* Watch ID associated with ALSA sequencer. */ + unsigned int watch_id_seq; + } midi; #endif diff --git a/src/bluez-midi.c b/src/bluez-midi.c index 1c7dc48aa..2e1806045 100644 --- a/src/bluez-midi.c +++ b/src/bluez-midi.c @@ -38,6 +38,7 @@ #include "bluealsa-config.h" #include "bluez-iface.h" #include "dbus.h" +#include "midi.h" #include "shared/bluetooth.h" #include "shared/defs.h" #include "shared/log.h" @@ -208,6 +209,8 @@ static void bluez_midi_characteristic_acquire_write( /* TODO: Find a way to detect "device" disconnection condition. */ + midi_transport_start_watch_ble_midi(t); + GUnixFDList *fd_list = g_unix_fd_list_new_from_array(&fds[1], 1); g_dbus_method_invocation_return_value_with_unix_fd_list(inv, g_variant_new("(hq)", 0, mtu), fd_list); @@ -262,6 +265,7 @@ static void bluez_midi_characteristic_acquire_notify( debug("New BLE-MIDI notify link (MTU: %u): %d", mtu, fds[0]); app->notify_acquired = true; t->midi.ble_fd_notify = fds[0]; + ble_midi_encode_set_mtu(&t->midi.ble_encoder, mtu); t->mtu_write = mtu; /* Setup IO watch for checking HUP condition on the socket. HUP means @@ -377,6 +381,8 @@ GDBusObjectManagerServer *bluez_midi_app_new( error("Couldn't create local MIDI transport: %s", strerror(errno)); else if (ba_transport_acquire(t) == -1) error("Couldn't acquire local MIDI transport: %s", strerror(errno)); + else if (ba_transport_start(t) == -1) + error("Couldn't start local MIDI transport: %s", strerror(errno)); app->t = t; GDBusObjectManagerServer *manager = g_dbus_object_manager_server_new(path); diff --git a/src/midi.c b/src/midi.c new file mode 100644 index 000000000..46bea30a6 --- /dev/null +++ b/src/midi.c @@ -0,0 +1,272 @@ +/* + * BlueALSA - midi.c + * Copyright (c) 2016-2023 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#include "midi.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "ba-transport.h" +#include "ble-midi.h" +#include "shared/log.h" + +static gboolean midi_watch_read_alsa_seq(G_GNUC_UNUSED GIOChannel *ch, + G_GNUC_UNUSED GIOCondition condition, void *userdata) { + + struct ba_transport *t = userdata; + unsigned char buf[1024]; + long len; + int rv; + + if (t->midi.ble_fd_notify == -1) { + /* Drop all events if notification is not acquired. */ + snd_seq_drop_input(t->midi.seq); + return TRUE; + } + + snd_seq_event_t *ev; + while (snd_seq_event_input(t->midi.seq, &ev) >= 0) { + + if ((len = snd_midi_event_decode(t->midi.seq_parser, buf, sizeof(buf), ev)) < 0) + error("Couldn't decode MIDI event: %s", snd_strerror(len)); + else { + +retry: + rv = ble_midi_encode(&t->midi.ble_encoder, buf, len); + if (rv == 1 || (rv == -1 && errno == EMSGSIZE)) { + /* Write out encoded BLE-MIDI packet to the socket. */ + if (write(t->midi.ble_fd_notify, t->midi.ble_encoder.buffer, + t->midi.ble_encoder.len) != (ssize_t)t->midi.ble_encoder.len) + error("BLE-MIDI link write error: %s", strerror(errno)); + if (rv == -1) { + /* Reset encoder state and try again. */ + t->midi.ble_encoder.len = 0; + goto retry; + } + } + else if (rv == -1) + error("Couldn't encode MIDI event: %s", strerror(errno)); + + } + + } + + /* Write out encoded BLE-MIDI packet to the socket. */ + if (write(t->midi.ble_fd_notify, t->midi.ble_encoder.buffer, + t->midi.ble_encoder.len) != (ssize_t)t->midi.ble_encoder.len) + error("BLE-MIDI link write error: %s", strerror(errno)); + t->midi.ble_encoder.len = 0; + + return TRUE; +} + +static gboolean midi_watch_read_ble_midi(GIOChannel *ch, + G_GNUC_UNUSED GIOCondition condition, void *userdata) { + + struct ba_transport *t = userdata; + GError *err = NULL; + uint8_t data[512]; + size_t len; + + switch (g_io_channel_read_chars(ch, (char *)data, sizeof(data), &len, &err)) { + case G_IO_STATUS_AGAIN: + return TRUE; + case G_IO_STATUS_ERROR: + error("BLE-MIDI link read error: %s", err->message); + g_error_free(err); + return TRUE; + case G_IO_STATUS_NORMAL: + break; + case G_IO_STATUS_EOF: + /* remove channel from watch */ + return FALSE; + } + + snd_seq_event_t ev = { 0 }; + snd_seq_ev_set_source(&ev, t->midi.seq_port); + snd_seq_ev_set_direct(&ev); + snd_seq_ev_set_subs(&ev); + + for (;;) { + + int rv; + if ((rv = ble_midi_decode(&t->midi.ble_decoder, data, len)) <= 0) { + if (rv == -1) { + error("Couldn't parse BLE-MIDI packet: %s", strerror(errno)); + hexdump("BLE-MIDI packet", data, len, true); + } + break; + } + + snd_midi_event_reset_encode(t->midi.seq_parser); + snd_midi_event_encode(t->midi.seq_parser, + t->midi.ble_decoder.buffer, t->midi.ble_decoder.len, &ev); + + if ((rv = snd_seq_event_output_direct(t->midi.seq, &ev)) < 0) + error("Couldn't send MIDI event: %s", snd_strerror(rv)); + + } + + return TRUE; +} + +int midi_transport_alsa_seq_create(struct ba_transport *t) { + + const struct ba_device *d = t->d; + snd_seq_client_info_t *info; + snd_seq_t *seq = NULL; + int rv; + + if ((rv = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, SND_SEQ_NONBLOCK)) != 0) { + error("Couldn't open ALSA sequencer: %s", snd_strerror(rv)); + goto fail; + } + + snd_seq_client_info_alloca(&info); + if ((rv = snd_seq_get_client_info(seq, info)) != 0) { + error("Couldn't get ALSA sequencer client info: %s", snd_strerror(rv)); + goto fail; + } + + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_NOTEON); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_NOTEOFF); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_KEYPRESS); + + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CONTROLLER); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_PGMCHANGE); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CHANPRESS); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_PITCHBEND); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CONTROL14); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_NONREGPARAM); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_REGPARAM); + + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_SONGPOS); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_SONGSEL); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_QFRAME); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_TIMESIGN); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_KEYSIGN); + + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_START); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CONTINUE); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_STOP); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CLOCK); + + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_TUNE_REQUEST); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_RESET); + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_SENSING); + + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_SYSEX); + + snd_seq_client_info_set_name(info, "BlueALSA"); + + if ((rv = snd_seq_set_client_info(seq, info)) != 0) { + error("Couldn't set ALSA sequencer client info: %s", snd_strerror(rv)); + goto fail; + } + + char addrstr[18]; + char name[10 + sizeof(addrstr)] = "BLE MIDI Server"; + if (bacmp(&d->addr, &d->a->hci.bdaddr) != 0) { + ba2str(&d->addr, addrstr); + snprintf(name, sizeof(name), "BLE MIDI %s", addrstr); + } + + if ((rv = snd_seq_create_simple_port(seq, name, + SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ | + SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_HARDWARE)) < 0) { + error("Couldn't create MIDI port: %s", snd_strerror(rv)); + goto fail; + } + + debug("Created new ALSA sequencer port: %d", rv); + t->midi.seq = seq; + t->midi.seq_port = rv; + + return 0; + +fail: + if (seq != NULL) + snd_seq_close(seq); + return -1; +} + +int midi_transport_alsa_seq_delete(struct ba_transport *t) { + + debug("Releasing ALSA sequencer port: %d", t->midi.seq_port); + + snd_seq_delete_simple_port(t->midi.seq, t->midi.seq_port); + t->midi.seq_port = -1; + snd_seq_close(t->midi.seq); + t->midi.seq = NULL; + + return 0; +} + +int midi_transport_start_watch_alsa_seq(struct ba_transport *t) { + + struct pollfd pfd; + snd_seq_poll_descriptors(t->midi.seq, &pfd, 1, POLLIN); + + debug("Starting ALSA sequencer IO watch: %d", pfd.fd); + + GIOChannel *ch_seq = g_io_channel_unix_new(pfd.fd); + g_io_channel_set_encoding(ch_seq, NULL, NULL); + g_io_channel_set_buffered(ch_seq, FALSE); + t->midi.watch_id_seq = g_io_add_watch_full(ch_seq, G_PRIORITY_HIGH, + G_IO_IN, midi_watch_read_alsa_seq, ba_transport_ref(t), + (GDestroyNotify)ba_transport_unref); + g_io_channel_unref(ch_seq); + + return 0; +} + +int midi_transport_start_watch_ble_midi(struct ba_transport *t) { + + debug("Starting BLE-MIDI IO watch: %d", t->midi.ble_fd_write); + + GIOChannel *ch_bt = g_io_channel_unix_new(t->midi.ble_fd_write); + g_io_channel_set_close_on_unref(ch_bt, TRUE); + g_io_channel_set_encoding(ch_bt, NULL, NULL); + g_io_channel_set_buffered(ch_bt, FALSE); + t->midi.watch_id_ble = g_io_add_watch_full(ch_bt, G_PRIORITY_HIGH, + G_IO_IN, midi_watch_read_ble_midi, ba_transport_ref(t), + (GDestroyNotify)ba_transport_unref); + g_io_channel_unref(ch_bt); + + return 0; +} + +int midi_transport_start(struct ba_transport *t) { + + /* Reset BLE-MIDI encoder/decoder states. */ + memset(&t->midi.ble_decoder, 0, sizeof(t->midi.ble_decoder)); + memset(&t->midi.ble_encoder, 0, sizeof(t->midi.ble_encoder)); + + midi_transport_start_watch_alsa_seq(t); + + return 0; +} + +int midi_transport_stop(struct ba_transport *t) { + + if (t->midi.watch_id_seq != 0) + g_source_remove(t->midi.watch_id_seq); + if (t->midi.watch_id_ble != 0) + g_source_remove(t->midi.watch_id_ble); + + return 0; +} diff --git a/src/midi.h b/src/midi.h new file mode 100644 index 000000000..6f38083ed --- /dev/null +++ b/src/midi.h @@ -0,0 +1,30 @@ +/* + * BlueALSA - midi.h + * Copyright (c) 2016-2023 Arkadiusz Bokowy + * + * This file is a part of bluez-alsa. + * + * This project is licensed under the terms of the MIT license. + * + */ + +#pragma once +#ifndef BLUEALSA_MIDI_H_ +#define BLUEALSA_MIDI_H_ + +#if HAVE_CONFIG_H +# include +#endif + +#include "ba-transport.h" + +int midi_transport_alsa_seq_create(struct ba_transport *t); +int midi_transport_alsa_seq_delete(struct ba_transport *t); + +int midi_transport_start_watch_alsa_seq(struct ba_transport *t); +int midi_transport_start_watch_ble_midi(struct ba_transport *t); + +int midi_transport_start(struct ba_transport *t); +int midi_transport_stop(struct ba_transport *t); + +#endif diff --git a/test/mock/Makefile.am b/test/mock/Makefile.am index be3d79c9f..048087ed9 100644 --- a/test/mock/Makefile.am +++ b/test/mock/Makefile.am @@ -37,6 +37,7 @@ bluealsa_mock_SOURCES = \ bluealsa_mock_CFLAGS = \ -I$(top_srcdir)/src \ -I$(top_srcdir)/test \ + @ALSA_CFLAGS@ \ @BLUEZ_CFLAGS@ \ @GIO2_CFLAGS@ \ @GLIB2_CFLAGS@ \ @@ -46,6 +47,7 @@ bluealsa_mock_CFLAGS = \ @SPANDSP_CFLAGS@ bluealsa_mock_LDADD = \ + @ALSA_LIBS@ \ @BLUEZ_LIBS@ \ @GIO2_LIBS@ \ @GLIB2_LIBS@ \ @@ -90,6 +92,12 @@ bluealsa_mock_CFLAGS += @LDAC_ABR_CFLAGS@ @LDAC_DEC_CFLAGS@ @LDAC_ENC_CFLAGS@ bluealsa_mock_LDADD += @LDAC_ABR_LIBS@ @LDAC_DEC_LIBS@ @LDAC_ENC_LIBS@ endif +if ENABLE_MIDI +bluealsa_mock_SOURCES += \ + ../../src/ble-midi.c \ + ../../src/midi.c +endif + if ENABLE_MPEG bluealsa_mock_SOURCES += ../../src/a2dp-mpeg.c bluealsa_mock_CFLAGS += @MPG123_CFLAGS@ diff --git a/test/test-ba.c b/test/test-ba.c index 987568e61..15882bf92 100644 --- a/test/test-ba.c +++ b/test/test-ba.c @@ -37,9 +37,8 @@ #include "bluealsa-dbus.h" #include "bluez.h" #include "hfp.h" -#if ENABLE_OFONO -# include "ofono.h" -#endif +#include "midi.h" +#include "ofono.h" #include "storage.h" #include "shared/a2dp-codecs.h" #include "shared/log.h" @@ -51,6 +50,10 @@ void a2dp_transport_init(struct ba_transport *t) { (void)t; } int a2dp_transport_start(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_alsa_seq_create(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_alsa_seq_delete(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_start(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_stop(struct ba_transport *t) { (void)t; return 0; } void *sco_enc_thread(struct ba_transport_pcm *t_pcm); void *ba_rfcomm_thread(struct ba_transport *t) { (void)t; return 0; } diff --git a/test/test-io.c b/test/test-io.c index 77f7a5903..e5e1fa5d2 100644 --- a/test/test-io.c +++ b/test/test-io.c @@ -70,6 +70,7 @@ #include "bluez.h" #include "hfp.h" #include "io.h" +#include "midi.h" #if ENABLE_OFONO # include "ofono.h" #endif @@ -129,6 +130,10 @@ bool bluez_a2dp_set_configuration(const char *current_dbus_sep_path, (void)current_dbus_sep_path; (void)sep; (void)error; return false; } int ofono_call_volume_update(struct ba_transport *t) { debug("%s: %p", __func__, t); (void)t; return 0; } +int midi_transport_alsa_seq_create(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_alsa_seq_delete(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_start(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_stop(struct ba_transport *t) { (void)t; return 0; } int storage_device_load(const struct ba_device *d) { (void)d; return 0; } int storage_device_save(const struct ba_device *d) { (void)d; return 0; } int storage_pcm_data_sync(struct ba_transport_pcm *pcm) { (void)pcm; return 0; } diff --git a/test/test-rfcomm.c b/test/test-rfcomm.c index 98048b221..707eeaa9c 100644 --- a/test/test-rfcomm.c +++ b/test/test-rfcomm.c @@ -60,6 +60,10 @@ static void dbus_update_counters_wait(unsigned int *counter, unsigned int value) void a2dp_transport_init(struct ba_transport *t) { (void)t; } int a2dp_transport_start(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_alsa_seq_create(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_alsa_seq_delete(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_start(struct ba_transport *t) { (void)t; return 0; } +int midi_transport_stop(struct ba_transport *t) { (void)t; return 0; } int storage_device_load(const struct ba_device *d) { (void)d; return 0; } int storage_device_save(const struct ba_device *d) { (void)d; return 0; } int storage_pcm_data_sync(struct ba_transport_pcm *pcm) { (void)pcm; return 0; }