Hi Luiz, On 09/12/16 13:06, Luiz Augusto von Dentz wrote: > Hi Felipe, > > On Fri, Dec 9, 2016 at 1:26 PM, Felipe Ferreri Tonello > <eu@xxxxxxxxxxxxxxxxx> wrote: >> CC'ing agoode. >> >> On 09/12/16 11:22, Felipe F. Tonello wrote: >>> This plugin implements the Central role of MIDI over Bluetooth >>> Low-Energy (BLE-MIDI) 1.0 specification as published by MMA in >>> November/2015. >>> >>> It was implmemented as a bluetoothd plugin because of latency requirements >>> of MIDI. There are still room for improvements on this regard. >>> >>> Like previsouly mentioned, it only implements the Central role, but >>> since all parsing and state-machine code is in libmidi.[hc] it should be >>> simple to implement the Peripheral role as a GATT service as well. >>> >>> I have also implemented several unit-tests that can be easily extended >>> without adding any code, just use-cases. >>> >>> Files added: >>> * profiles/midi/midi.c: Actual GATT plugin >>> * profiles/midi/libmidi.[ch]: MIDI parsers >>> * unit/test-midi.c: Unit-tests >>> >>> Techinal notes >>> ============== >>> >>> This plugin doesn't require any new threads. It relies on notifications >>> from a device to parse and render proper events that are queued in the >>> kernel, causing no blocks at all. Even if an error occur, it will be >>> handled and returned control to bluetoothd. >>> >>> It also adds a new file descriptor to be read using struct io. That is >>> necessary to read events from applications and render raw BLE packets to >>> be sent to the device with a write without response command. It doesn't >>> block as well. >>> >>> ALSA dependency was added since there is no other way of doing it, >>> sorry for it. :/ But this can be easily disabled with --disable-alsa >>> configure flag. >>> >>> Even though this introduces ALSA dependency, it is not an audio plugin. >>> It is rather a MIDI plugin, which is a byte stream protocol with low >>> throughput but requires low-latency. > > Lets have a --enable-midi for now so it is only build if the > distro/user wants it otherwise it is disabled. Ok. Makes sense not to add automatically ALSA this dependency at first release. > >>> Observations >>> ============ >>> >>> I have tested on a normal laptop Arch-linux (x86_64) and a Raspberry Pi 2 >>> (ARM Cortex-A8) and it works very well. As I mentioned, the latency can >>> always be improved. >>> >>> I will still maintain a personal branch on my github[1] so others can >>> contribute there and I can test before sending to bluez. >>> >>> IMPORTAT: the timestamp support is incomplete since ALSA doesn't support the >>> way MIDI over BLE expects (asign timestamp to an event without scheduling). >>> We are working on ALSA to improve this. >>> >>> Credits >>> ======= >>> >>> I would like to send kudos to ROLI Ltd. which allowed my to work >>> on this as part of my full-time job. >>> >>> [1] https://github.com/ftonello/bluez/ >>> --- >>> >>> Changes from v2: >>> * Removed _GNU_SOURCE (not used) >>> * Appended " Bluetooth" to the ALSA-Seq port name >>> * Changed configuration from alsa to midi (--disable-midi) >>> >>> Changes from v1: >>> * Fixed typos in commit message >>> * Minor improves on the SysEx parser >>> >>> Makefile.am | 13 +- >>> Makefile.plugins | 8 + >>> configure.ac | 11 ++ >>> profiles/midi/libmidi.c | 385 ++++++++++++++++++++++++++++++++++++++ >>> profiles/midi/libmidi.h | 140 ++++++++++++++ >>> profiles/midi/midi.c | 489 ++++++++++++++++++++++++++++++++++++++++++++++++ >>> unit/test-midi.c | 464 +++++++++++++++++++++++++++++++++++++++++++++ >>> 7 files changed, 1509 insertions(+), 1 deletion(-) >>> create mode 100644 profiles/midi/libmidi.c >>> create mode 100644 profiles/midi/libmidi.h >>> create mode 100644 profiles/midi/midi.c >>> create mode 100644 unit/test-midi.c >>> >>> diff --git a/Makefile.am b/Makefile.am >>> index c469a6caf83a..b7ae2a31d671 100644 >>> --- a/Makefile.am >>> +++ b/Makefile.am >>> @@ -147,6 +147,7 @@ gobex_sources = gobex/gobex.h gobex/gobex.c \ >>> builtin_modules = >>> builtin_sources = >>> builtin_nodist = >>> +builtin_ldadd = >>> >>> include Makefile.plugins >>> >>> @@ -191,7 +192,8 @@ src_bluetoothd_SOURCES = $(builtin_sources) \ >>> src_bluetoothd_LDADD = lib/libbluetooth-internal.la \ >>> gdbus/libgdbus-internal.la \ >>> src/libshared-glib.la \ >>> - @BACKTRACE_LIBS@ @GLIB_LIBS@ @DBUS_LIBS@ -ldl -lrt >>> + @BACKTRACE_LIBS@ @GLIB_LIBS@ @DBUS_LIBS@ -ldl -lrt \ >>> + $(builtin_ldadd) >>> src_bluetoothd_LDFLAGS = $(AM_LDFLAGS) -Wl,--export-dynamic \ >>> -Wl,--version-script=$(srcdir)/src/bluetooth.ver >>> >>> @@ -421,6 +423,15 @@ unit_test_gattrib_LDADD = lib/libbluetooth-internal.la \ >>> src/libshared-glib.la \ >>> @GLIB_LIBS@ @DBUS_LIBS@ -ldl -lrt >>> >>> +if MIDI >>> +unit_tests += unit/test-midi >>> +unit_test_midi_SOURCES = unit/test-midi.c \ >>> + profiles/midi/libmidi.h \ >>> + profiles/midi/libmidi.c >>> +unit_test_midi_LDADD = src/libshared-glib.la \ >>> + @GLIB_LIBS@ @ALSA_LIBS@ >>> +endif >>> + >>> if MAINTAINER_MODE >>> noinst_PROGRAMS += $(unit_tests) >>> endif >>> diff --git a/Makefile.plugins b/Makefile.plugins >>> index 59342c0cb803..3a9e27c653dd 100644 >>> --- a/Makefile.plugins >>> +++ b/Makefile.plugins >>> @@ -95,6 +95,14 @@ builtin_sources += profiles/scanparam/scan.c >>> builtin_modules += deviceinfo >>> builtin_sources += profiles/deviceinfo/deviceinfo.c >>> >>> +if MIDI >>> +builtin_modules += midi >>> +builtin_sources += profiles/midi/midi.c \ >>> + profiles/midi/libmidi.h \ >>> + profiles/midi/libmidi.c >>> +builtin_ldadd += @ALSA_LIBS@ >>> +endif >>> + >>> if SIXAXIS >>> plugin_LTLIBRARIES += plugins/sixaxis.la >>> plugins_sixaxis_la_SOURCES = plugins/sixaxis.c >>> diff --git a/configure.ac b/configure.ac >>> index fe4103c19037..a0641de98c1d 100644 >>> --- a/configure.ac >>> +++ b/configure.ac >>> @@ -212,6 +212,17 @@ AC_ARG_ENABLE(cups, AC_HELP_STRING([--disable-cups], >>> [disable CUPS printer support]), [enable_cups=${enableval}]) >>> AM_CONDITIONAL(CUPS, test "${enable_cups}" != "no") >>> >>> +AC_ARG_ENABLE(midi, AC_HELP_STRING([--disable-midi], >>> + [disable MIDI support]), [enable_alsa=${enableval}]) >>> +AM_CONDITIONAL(MIDI, test "${enable_midi}" != "no") >>> + >>> +if (test "${enable_midi}" != "no"); then >>> + PKG_CHECK_MODULES(ALSA, alsa, dummy=yes, >>> + AC_MSG_ERROR(ALSA lib is required for MIDI support)) >>> + AC_SUBST(ALSA_CFLAGS) >>> + AC_SUBST(ALSA_LIBS) >>> +fi >>> + > > Lets have the other way around so the user/distro has to opt-in. Ok. > >>> AC_ARG_ENABLE(obex, AC_HELP_STRING([--disable-obex], >>> [disable OBEX profile support]), [enable_obex=${enableval}]) >>> if (test "${enable_obex}" != "no"); then >>> diff --git a/profiles/midi/libmidi.c b/profiles/midi/libmidi.c >>> new file mode 100644 >>> index 000000000000..001444b9c700 >>> --- /dev/null >>> +++ b/profiles/midi/libmidi.c >>> @@ -0,0 +1,385 @@ >>> +/* >>> + * >>> + * BlueZ - Bluetooth protocol stack for Linux >>> + * >>> + * Copyright (C) 2015,2016 Felipe F. Tonello <eu@xxxxxxxxxxxxxxxxx> >>> + * Copyright (C) 2016 ROLI Ltd. >>> + * >>> + * This program is free software; you can redistribute it and/or modify >>> + * it under the terms of the GNU General Public License as published by >>> + * the Free Software Foundation; either version 2 of the License, or >>> + * (at your option) any later version. >>> + * >>> + * This program is distributed in the hope that it will be useful, >>> + * but WITHOUT ANY WARRANTY; without even the implied warranty of >>> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >>> + * GNU General Public License for more details. >>> + * >>> + * You should have received a copy of the GNU General Public License >>> + * along with this program; if not, write to the Free Software >>> + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA >>> + * >>> + * >>> + */ >>> + >>> +#include "libmidi.h" >>> + >>> +inline static void append_timestamp_high_maybe(struct midi_write_parser *parser) >>> +{ >>> + if (!midi_write_has_data(parser)) { >>> + guint8 timestamp_high = 0x80; >>> + parser->rtime = g_get_monotonic_time() / 1000; /* convert µs to ms */ >>> + timestamp_high |= (parser->rtime & 0x1F80) >> 7; >>> + /* set timestampHigh */ >>> + g_byte_array_append(parser->midi_stream, ×tamp_high, >>> + sizeof(timestamp_high)); >>> + } >>> +} >>> + >>> +inline static void append_timestamp_low(struct midi_write_parser *parser) >>> +{ >>> + const guint8 timestamp_low = 0x80 | (parser->rtime & 0x7F); >>> + g_byte_array_append(parser->midi_stream, ×tamp_low, >>> + sizeof(timestamp_low)); >>> +} >>> + >>> +void midi_read_ev(struct midi_write_parser *parser, const snd_seq_event_t *ev, >>> + midi_read_ev_cb write_cb, gpointer user_data) >>> +{ >>> + g_assert(write_cb); >>> + >>> +#define GET_AVAILABLE_SIZE (parser->stream_size - parser->midi_stream->len) > > Lets avoid using defines inside the scope of a function, actually for > this one you could add a helper function to calculate this passing a > parser as argument. Fair enough. > >>> + >>> + if (GET_AVAILABLE_SIZE == 0) { >>> + write_cb(parser->midi_stream->data, parser->midi_stream->len, >>> + user_data); > > Align to right when the line doesn't into 80 columns. Sorry, could you please clarify this? > >>> + midi_write_reset(parser); >>> + } >>> + >>> + append_timestamp_high_maybe(parser); >>> + >>> + /* SysEx is special case: >>> + SysEx has two timestampLow bytes, before F0 and F7 >>> + */ >>> + if (ev->type == SND_SEQ_EVENT_SYSEX) { >>> + >>> + unsigned int used_sysex = 0; >>> + >>> + /* Algorithm: >>> + 1) check initial timestampLow: >>> + if used_sysex == 0, then tsLow = 1, else tsLow = 0 >>> + 2) calculate sysex size of current packet: >>> + 2a) first check special case: >>> + if midi->out_length - 1 (tsHigh) - tsLow == >>> + sysex_length - used_sysex >>> + size is: min(midi->out_length - 1 - tsLow, >>> + sysex_length - used_sysex - 1) >>> + 2b) else size is: min(midi->out_length - 1 - tsLow, >>> + sysex_length - used_sysex) >>> + 3) check if packet contains F7: fill respective tsLow byte >>> + */ >>> + >>> +#define GET_SYSEX_IDX(_i) (((guint8*)ev->data.ext.ptr)[_i]) >>> +#define GET_DATA_REVERSE_IDX(_i) (parser->midi_stream->data \ >>> + [parser->midi_stream->len - ((_i) + 1)]) > > Again, lets not use defines/macros inside the scope of functions. Btw > this whole thing is probably worth moving into a dedicated function Right. > >>> + >>> + /* We need at least 2 bytes (timestampLow + F0) */ >>> + if (GET_AVAILABLE_SIZE < 2) { >>> + /* send current message and start new one */ >>> + write_cb(parser->midi_stream->data, parser->midi_stream->len, >>> + user_data); >>> + midi_write_reset(parser); >>> + append_timestamp_high_maybe(parser); >>> + } >>> + >>> + /* timestampLow on initial F0 */ >>> + if (GET_SYSEX_IDX(0) == 0xF0) >>> + append_timestamp_low(parser); >>> + >>> + do { >>> + unsigned int size_of_sysex; >>> + >>> + append_timestamp_high_maybe(parser); >>> + >>> + size_of_sysex = MIN(GET_AVAILABLE_SIZE, >>> + ev->data.ext.len - used_sysex); >>> + >>> + if (GET_AVAILABLE_SIZE == ev->data.ext.len - used_sysex) >>> + size_of_sysex--; >>> + >>> + g_byte_array_append(parser->midi_stream, >>> + ev->data.ext.ptr + used_sysex, >>> + size_of_sysex); >>> + used_sysex += size_of_sysex; >>> + >>> + if (GET_AVAILABLE_SIZE <= 1 && GET_DATA_REVERSE_IDX(0) != 0xF7) { >>> + write_cb(parser->midi_stream->data, parser->midi_stream->len, >>> + user_data); >>> + midi_write_reset(parser); >>> + } >>> + } while (used_sysex < ev->data.ext.len); >>> + >>> + /* check for F7 and update respective timestampLow byte */ >>> + if (GET_DATA_REVERSE_IDX(0) == 0xF7) { >>> + guint8 tmp; >>> + append_timestamp_low(parser); >>> + tmp = GET_DATA_REVERSE_IDX(0); >>> + GET_DATA_REVERSE_IDX(0) = 0xF7; >>> + GET_DATA_REVERSE_IDX(1) = tmp; >>> + } >>> + >>> +#undef GET_DATA_REVERSE_IDX >>> +#undef GET_SYSEX_IDX >>> + >>> + } else { >>> + int length; >>> + >>> + /* check for running status */ >>> + if (parser->rstate != ev->type) { >>> + snd_midi_event_reset_decode(parser->midi_ev); >>> + append_timestamp_low(parser); >>> + } >>> + >>> + /* each midi message has timestampLow byte to follow */ >>> + length = snd_midi_event_decode(parser->midi_ev, >>> + parser->midi_stream->data + >>> + parser->midi_stream->len, >>> + GET_AVAILABLE_SIZE, >>> + ev); >>> + >>> + if (length == -ENOMEM) { >>> + /* remove previously added timestampLow */ >>> + g_byte_array_remove_index_fast(parser->midi_stream, >>> + parser->midi_stream->len -1); >>> + write_cb(parser->midi_stream->data, parser->midi_stream->len, >>> + user_data); >>> + /* cleanup state for next packet */ >>> + snd_midi_event_reset_decode(parser->midi_ev); >>> + midi_write_reset(parser); >>> + append_timestamp_high_maybe(parser); >>> + append_timestamp_low(parser); >>> + length = snd_midi_event_decode(parser->midi_ev, >>> + parser->midi_stream->data + >>> + parser->midi_stream->len, >>> + GET_AVAILABLE_SIZE, >>> + ev); >>> + } >>> + >>> + if (length > 0) >>> + parser->midi_stream->len += length; >>> + } >>> + >>> + parser->rstate = ev->type; >>> + >>> + if (GET_AVAILABLE_SIZE <= 1) { >>> + write_cb(parser->midi_stream->data, parser->midi_stream->len, >>> + user_data); >>> + midi_write_reset(parser); >>> + } >>> +#undef GET_AVAILABLE_SIZE >>> +} >>> + >>> +static void update_ev_timestamp(struct midi_read_parser *parser, snd_seq_event_t *ev, >>> + guint16 ts_low) >>> +{ >>> + int delta_timestamp; >>> + int delta_rtime; >>> + gint64 rtime_current; >>> + guint16 timestamp; >>> + >>> + /* time_low overwflow results on time_high to increment by one */ >>> + if (parser->timestamp_low > ts_low) >>> + parser->timestamp_high++; >>> + >>> + timestamp = (parser->timestamp_high << 7) | parser->timestamp_low; >>> + >>> + rtime_current = g_get_monotonic_time() / 1000; /* convert µs to ms */ >>> + delta_timestamp = timestamp - (int)parser->timestamp; >>> + delta_rtime = rtime_current - parser->rtime; >>> + >>> + if (delta_rtime > MIDI_MAX_TIMESTAMP) >>> + parser->rtime = rtime_current; >>> + else { >>> + >>> + /* If delta_timestamp is way to big than delta_rtime, >>> + this means that the device sent a message in the past, >>> + so we have to compensate for this. */ >>> + if (delta_timestamp > 7000 && delta_rtime < 1000) >>> + delta_timestamp = 0; >>> + >>> + /* check if timestamp did overflow */ >>> + if (delta_timestamp < 0) { >>> + /* same timestamp in the past problem */ >>> + if ((delta_timestamp + MIDI_MAX_TIMESTAMP) > 7000 && >>> + delta_rtime < 1000) >>> + delta_timestamp = 0; >>> + else >>> + delta_timestamp = delta_timestamp + MIDI_MAX_TIMESTAMP; >>> + } >>> + >>> + parser->rtime += delta_timestamp; >>> + } >>> + >>> + parser->timestamp += delta_timestamp; >>> + if (parser->timestamp > MIDI_MAX_TIMESTAMP) >>> + parser->timestamp %= MIDI_MAX_TIMESTAMP + 1; >>> + >>> + /* set event timestamp */ >>> + /* TODO: update event timestamp here! */ >>> +} >>> + >>> +gsize midi_read_raw(struct midi_read_parser *parser, const guint8 *data, >>> + gsize size, snd_seq_event_t *ev /* OUT */) >>> +{ >>> + guint8 midi_size = 0; >>> + gsize i = 0; >>> + gboolean err = FALSE; >>> + guint8 time_low = 0; >>> + >>> + if (parser->timestamp_high == 0) >>> + parser->timestamp_high = data[i++] & 0x3F; >>> + >>> + snd_midi_event_reset_encode(parser->midi_ev); >>> + >>> + /* timestamp byte */ >>> + if (data[i] & 0x80) { >>> + update_ev_timestamp(parser, ev, data[i] & 0x7F); >>> + >>> + /* check for wrong BLE-MIDI message size */ >>> + if (++i == size) { >>> + err = TRUE; >>> + goto _finish; >>> + } >>> + } >>> + >>> + /* cleanup sysex_stream if message is broken or is a new SysEx */ >>> + if (data[i] >= 0x80 && data[i] != 0xF7 && parser->sysex_stream->len > 0) { >>> + g_byte_array_remove_range(parser->sysex_stream, >>> + 0, >>> + parser->sysex_stream->len); >>> + } >>> + >>> + switch (data[i]) { >>> + case 0xF8 ... 0XFF: >>> + /* System Real-Time Messages */ >>> + midi_size = 1; >>> + break; >>> + >>> + /* System Common Messages */ >>> + case 0xF0: /* SysEx Start */ { >>> + guint8 *pos; >>> + >>> + /* Avoid parsing if SysEx is contained in one BLE packet */ >>> + if ((pos = memchr(data + i, 0xF7, size - i))) { >>> + const gsize sysex_length = pos - (data + i); >>> + >>> + g_byte_array_append(parser->sysex_stream, data + i, sysex_length); >>> + >>> + time_low = parser->sysex_stream->data[sysex_length - 1] & 0x7F; >>> + /* Remove timestamp byte */ >>> + parser->sysex_stream->data[sysex_length - 1] = 0xF7; >>> + update_ev_timestamp(parser, ev, time_low); >>> + snd_seq_ev_set_sysex(ev, parser->sysex_stream->len, >>> + parser->sysex_stream->data); >>> + >>> + midi_size = sysex_length + 1; /* +1 because of timestampLow */ >>> + } else { >>> + g_byte_array_append(parser->sysex_stream, data + i, size - i); >>> + err = TRUE; /* Not an actual error, just incomplete message */ >>> + midi_size = size - i; >>> + } >>> + >>> + goto _finish; >>> + } >>> + >>> + case 0xF1: >>> + case 0xF3: >>> + midi_size = 2; >>> + break; >>> + case 0xF2: >>> + midi_size = 3; >>> + break; >>> + case 0xF4: >>> + case 0xF5: /* Ignore */ >>> + i++; >>> + err = TRUE; >>> + goto _finish; >>> + break; >>> + case 0xF6: >>> + midi_size = 1; >>> + break; >>> + case 0xF7: /* SysEx End */ >>> + g_byte_array_append(parser->sysex_stream, data + i, 1); >>> + snd_seq_ev_set_sysex(ev, parser->sysex_stream->len, >>> + parser->sysex_stream->data); >>> + >>> + midi_size = 1; /* timestampLow was alredy processed */ >>> + goto _finish; >>> + >>> + case 0x80 ... 0xEF: >>> + /* >>> + * Channel Voice Messages, Channel Mode Messages >>> + * and Control Change Messages. >>> + */ >>> + parser->rstatus = data[i]; >>> + midi_size = (data[i] >= 0xC0 && data[i] <= 0xDF) ? 2 : 3; >>> + break; >>> + >>> + case 0x00 ... 0x7F: >>> + >>> + /* Check for SysEx messages */ >>> + if (parser->sysex_stream->len > 0) { >>> + guint8 *pos; >>> + >>> + if ((pos = memchr(data + i, 0xF7, size - i))) { >>> + const gsize sysex_length = pos - (data + i); >>> + >>> + g_byte_array_append(parser->sysex_stream, data + i, sysex_length); >>> + >>> +#define GET_DATA_REVERSE_IDX(_i) (parser->sysex_stream->data \ >>> + [parser->sysex_stream->len - ((_i) + 1)]) >>> + >>> + time_low = GET_DATA_REVERSE_IDX(0) & 0x7F; >>> + /* Remove timestamp byte */ >>> + GET_DATA_REVERSE_IDX(0) = 0xF7; >>> + update_ev_timestamp(parser, ev, time_low); >>> + snd_seq_ev_set_sysex(ev, parser->sysex_stream->len, >>> + parser->sysex_stream->data); >>> + >>> +#undef GET_DATA_REVERSE_IDX >>> + >>> + midi_size = sysex_length + 1; /* +1 because of timestampLow */ >>> + } else { >>> + g_byte_array_append(parser->sysex_stream, data + i, size - i); >>> + err = TRUE; /* Not an actual error, just incomplete message */ >>> + midi_size = size - i; >>> + } >>> + >>> + goto _finish; >>> + } >>> + >>> + /* Running State Message */ >>> + if (parser->rstatus == 0) { >>> + midi_size = 1; >>> + err = TRUE; >>> + goto _finish; >>> + } >>> + >>> + snd_midi_event_encode_byte(parser->midi_ev, parser->rstatus, ev); >>> + midi_size = (parser->rstatus >= 0xC0 && parser->rstatus <= 0xDF) ? 1 : 2; >>> + break; >>> + } >>> + >>> + if ((i + midi_size) > size) { >>> + err = TRUE; >>> + goto _finish; >>> + } >>> + >>> + snd_midi_event_encode(parser->midi_ev, data + i, midi_size, ev); >>> + >>> +_finish: >>> + if (err) >>> + ev->type = SND_SEQ_EVENT_NONE; >>> + >>> + return i + midi_size; >>> +} > > If the idea is to have a possible reusable library I would consider > splitting the library part which can probably go to src/shared and > then be licensed under LGPL. That's the idea. But what's the benefit of putting on src/shared? At the moment there is no use of this library outside a MIDI GATT driver. > >>> diff --git a/profiles/midi/libmidi.h b/profiles/midi/libmidi.h >>> new file mode 100644 >>> index 000000000000..cc6e13f6c44a >>> --- /dev/null >>> +++ b/profiles/midi/libmidi.h >>> @@ -0,0 +1,140 @@ >>> +/* >>> + * >>> + * BlueZ - Bluetooth protocol stack for Linux >>> + * >>> + * Copyright (C) 2015,2016 Felipe F. Tonello <eu@xxxxxxxxxxxxxxxxx> >>> + * Copyright (C) 2016 ROLI Ltd. >>> + * >>> + * This program is free software; you can redistribute it and/or modify >>> + * it under the terms of the GNU General Public License as published by >>> + * the Free Software Foundation; either version 2 of the License, or >>> + * (at your option) any later version. >>> + * >>> + * This program is distributed in the hope that it will be useful, >>> + * but WITHOUT ANY WARRANTY; without even the implied warranty of >>> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >>> + * GNU General Public License for more details. >>> + * >>> + * You should have received a copy of the GNU General Public License >>> + * along with this program; if not, write to the Free Software >>> + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA >>> + * >>> + * >>> + */ >>> + >>> +#ifndef LIBMIDI_H >>> +#define LIBMIDI_H >>> + >>> +#include <glib.h> >>> +#include <alsa/asoundlib.h> >>> + >>> +#define MIDI_MAX_TIMESTAMP 8191 >>> +#define MIDI_MSG_MAX_SIZE 12 >>> +#define MIDI_SYSEX_MAX_SIZE (4 * 1024) >>> + >>> +/* MIDI I/O Write parser */ >>> + >>> +struct midi_write_parser { >>> + gint64 rtime; /* last writer's real time */ >>> + snd_seq_event_type_t rstate; /* running status event type */ >>> + GByteArray *midi_stream; /* MIDI I/O byte stream */ >>> + gsize stream_size; /* what is the maximum size of the midi_stream array */ >>> + snd_midi_event_t *midi_ev; /* midi<->seq event */ >>> +}; >>> + >>> +static inline int midi_write_init(struct midi_write_parser *parser, gsize buffer_size) >>> +{ >>> + parser->rtime = 0; >>> + parser->rstate = SND_SEQ_EVENT_NONE; >>> + parser->stream_size = buffer_size; >>> + >>> + parser->midi_stream = g_byte_array_sized_new(buffer_size); >>> + if (!parser->midi_stream) >>> + return -ENOMEM; >>> + >>> + return snd_midi_event_new(buffer_size, &parser->midi_ev); >>> +} >>> + >>> +static inline void midi_write_free(struct midi_write_parser *parser) >>> +{ >>> + g_byte_array_free(parser->midi_stream, TRUE); >>> + snd_midi_event_free(parser->midi_ev); >>> +} >>> + >>> +static inline void midi_write_reset(struct midi_write_parser *parser) >>> +{ >>> + parser->rstate = SND_SEQ_EVENT_NONE; >>> + g_byte_array_remove_range(parser->midi_stream, 0, parser->midi_stream->len); >>> +} >>> + >>> +static inline gboolean midi_write_has_data(struct midi_write_parser *parser) >>> +{ >>> + return parser->midi_stream->len > 0; >>> +} >>> + >>> +static inline const guint8 * midi_write_data(struct midi_write_parser *parser) >>> +{ >>> + return parser->midi_stream->data; >>> +} >>> + >>> +static inline guint midi_write_data_size(struct midi_write_parser *parser) >>> +{ >>> + return parser->midi_stream->len; >>> +} >>> + >>> +typedef void (*midi_read_ev_cb)(const guint8 *, guint, gpointer); >>> + >>> +/* It creates BLE-MIDI raw packets from the a sequencer event. If the packet >>> + is full, then it calls write_cb and resets its internal state as many times >>> + as necessary. >>> + */ >>> +void midi_read_ev(struct midi_write_parser *parser, const snd_seq_event_t *ev, >>> + midi_read_ev_cb write_cb, gpointer user_data); >>> + >>> +/* MIDI I/O Read parser */ >>> + >>> +struct midi_read_parser { >>> + guint8 rstatus; /* running status byte */ >>> + gint64 rtime; /* last reader's real time */ >>> + gint16 timestamp; /* last MIDI-BLE timestamp */ >>> + gint8 timestamp_low; /* MIDI-BLE timestampLow from the current packet */ >>> + gint8 timestamp_high; /* MIDI-BLE timestampHigh from the current packet */ >>> + GByteArray *sysex_stream; /* SysEx stream */ >>> + snd_midi_event_t *midi_ev; /* midi<->seq event */ > > Avoid using glib types, instead use inttypes, etc. Perhaps only leave > GByteArray if you really need it for something special, is that used > for a ring buffer? Ok. No, it's not used for a ring buffer. I used initially to have a smarter allocation strategy for this buffer, but I ended up fixing it to MIDI_SYSEX_MAX_SIZE which is 4K. Perhaps I can just use a normal struct with data pointer and size. > >>> +}; >>> + >>> +static inline int midi_read_init(struct midi_read_parser *parser) >>> +{ >>> + parser->rstatus = 0; >>> + parser->rtime = -1; >>> + parser->timestamp = 0; >>> + parser->timestamp_low = 0; >>> + parser->timestamp_high = 0; >>> + >>> + parser->sysex_stream = g_byte_array_sized_new(MIDI_SYSEX_MAX_SIZE); >>> + if (!parser->sysex_stream) >>> + return -ENOMEM; >>> + >>> + return snd_midi_event_new(MIDI_MSG_MAX_SIZE, &parser->midi_ev); >>> +} >>> + >>> +static inline void midi_read_free(struct midi_read_parser *parser) >>> +{ >>> + g_byte_array_free(parser->sysex_stream, TRUE); >>> + snd_midi_event_free(parser->midi_ev); >>> +} >>> + >>> +static inline void midi_read_reset(struct midi_read_parser *parser) >>> +{ >>> + parser->rstatus = 0; >>> + parser->timestamp_low = 0; >>> + parser->timestamp_high = 0; >>> +} >>> + >>> +/* Parses raw BLE-MIDI messages and populates a sequencer event representing the >>> + current MIDI message. It returns how much raw data was processed. >>> + */ >>> +gsize midi_read_raw(struct midi_read_parser *parser, const guint8 *data, gsize size, >>> + snd_seq_event_t *ev /* OUT */); >>> + >>> +#endif /* LIBMIDI_H */ >>> diff --git a/profiles/midi/midi.c b/profiles/midi/midi.c >>> new file mode 100644 >>> index 000000000000..93a3023e8ba5 >>> --- /dev/null >>> +++ b/profiles/midi/midi.c >>> @@ -0,0 +1,489 @@ >>> +/* >>> + * >>> + * BlueZ - Bluetooth protocol stack for Linux >>> + * >>> + * Copyright (C) 2015,2016 Felipe F. Tonello <eu@xxxxxxxxxxxxxxxxx> >>> + * Copyright (C) 2016 ROLI Ltd. >>> + * >>> + * This program is free software; you can redistribute it and/or modify >>> + * it under the terms of the GNU General Public License as published by >>> + * the Free Software Foundation; either version 2 of the License, or >>> + * (at your option) any later version. >>> + * >>> + * This program is distributed in the hope that it will be useful, >>> + * but WITHOUT ANY WARRANTY; without even the implied warranty of >>> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >>> + * GNU General Public License for more details. >>> + * >>> + * You should have received a copy of the GNU General Public License >>> + * along with this program; if not, write to the Free Software >>> + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA >>> + * >>> + * Information about this plugin: >>> + * >>> + * This plugin implements the MIDI over Bluetooth Low-Energy (BLE-MIDI) 1.0 >>> + * specification as published by MMA in November/2015. >>> + * >>> + */ >>> + >>> +#ifdef HAVE_CONFIG_H >>> +#include <config.h> >>> +#endif >>> + >>> +#include <errno.h> >>> +#include <glib.h> >>> +#include <alsa/asoundlib.h> >>> + >>> +#include "lib/bluetooth.h" >>> +#include "lib/sdp.h" >>> +#include "lib/uuid.h" >>> + >>> +#include "src/plugin.h" >>> +#include "src/adapter.h" >>> +#include "src/device.h" >>> +#include "src/profile.h" >>> +#include "src/service.h" >>> +#include "src/shared/util.h" >>> +#include "src/shared/att.h" >>> +#include "src/shared/queue.h" >>> +#include "src/shared/gatt-db.h" >>> +#include "src/shared/gatt-client.h" >>> +#include "src/shared/io.h" >>> +#include "src/log.h" >>> +#include "attrib/att.h" >>> + >>> +#include "libmidi.h" >>> + >>> +#define MIDI_UUID "03B80E5A-EDE8-4B33-A751-6CE34EC4C700" >>> +#define MIDI_IO_UUID "7772E5DB-3868-4112-A1A9-F2669D106BF3" >>> + >>> +struct midi { >>> + struct btd_device *dev; >>> + struct gatt_db *db; >>> + struct bt_gatt_client *client; >>> + guint io_cb_id; >>> + struct io *io; >>> + uint16_t midi_io_handle; >>> + >>> + /* ALSA handlers */ >>> + snd_seq_t *seq_handle; >>> + int seq_client_id; >>> + int seq_port_id; >>> + >>> + /* MIDI parser*/ >>> + struct midi_read_parser midi_in; >>> + struct midi_write_parser midi_out; >>> +}; >>> + >>> +static bool midi_write_cb(struct io *io, gpointer user_data) >>> +{ >>> + struct midi *midi = user_data; >>> + int err; >>> + >>> + void foreach_cb(const guint8 *data, guint size, gpointer user_data) { >>> + struct midi *midi = user_data; >>> + bt_gatt_client_write_without_response(midi->client, >>> + midi->midi_io_handle, >>> + FALSE, >>> + data, >>> + size); >>> + }; >>> + >>> + do { >>> + snd_seq_event_t *event = NULL; >>> + >>> + err = snd_seq_event_input(midi->seq_handle, &event); >>> + >>> + if (err < 0 || !event) >>> + break; >>> + >>> + midi_read_ev(&midi->midi_out, event, foreach_cb, midi); >>> + >>> + } while (err > 0); >>> + >>> + if (midi_write_has_data(&midi->midi_out)) >>> + bt_gatt_client_write_without_response(midi->client, >>> + midi->midi_io_handle, >>> + FALSE, >>> + midi_write_data(&midi->midi_out), >>> + midi_write_data_size(&midi->midi_out)); >>> + >>> + midi_write_reset(&midi->midi_out); >>> + >>> + return TRUE; >>> +} >>> + >>> +static void midi_io_value_cb(uint16_t value_handle, const uint8_t *value, >>> + uint16_t length, gpointer user_data) >>> +{ >>> + struct midi *midi = user_data; >>> + snd_seq_event_t ev; >>> + guint i = 0; >>> + >>> + if (length < 3) { >>> + warn("MIDI I/O: Wrong packet format: length is %u bytes but it should " >>> + "be at least 3 bytes", length); >>> + return; >>> + } >>> + >>> + snd_seq_ev_clear(&ev); >>> + snd_seq_ev_set_source(&ev, midi->seq_port_id); >>> + snd_seq_ev_set_subs(&ev); >>> + snd_seq_ev_set_direct(&ev); >>> + >>> + midi_read_reset(&midi->midi_in); >>> + >>> + while (i < length) { >>> + gsize count = midi_read_raw(&midi->midi_in, value + i, length - i, &ev); >>> + >>> + if (count == 0) >>> + goto _err; >>> + >>> + if (ev.type != SND_SEQ_EVENT_NONE) >>> + snd_seq_event_output_direct(midi->seq_handle, &ev); >>> + >>> + i += count; >>> + } >>> + >>> + return; >>> + >>> +_err: >>> + error("Wrong BLE-MIDI message"); >>> +} >>> + >>> +static void midi_io_ccc_written_cb(uint16_t att_ecode, gpointer user_data) >>> +{ >>> + if (att_ecode != 0) { >>> + error("MIDI I/O: notifications not enabled %s", >>> + att_ecode2str(att_ecode)); >>> + return; >>> + } >>> + >>> + DBG("MIDI I/O: notification enabled"); >>> +} >>> + >>> +static void midi_io_initial_read_cb(bool success, uint8_t att_ecode, >>> + const uint8_t *value, uint16_t length, >>> + gpointer user_data) >>> +{ >>> + struct midi *midi = user_data; >>> + >>> + if (!success) { >>> + error("MIDI I/O: Failed to read initial request"); >>> + return; >>> + } >>> + >>> + /* request notify */ >>> + midi->io_cb_id = >>> + bt_gatt_client_register_notify(midi->client, >>> + midi->midi_io_handle, >>> + midi_io_ccc_written_cb, >>> + midi_io_value_cb, >>> + midi, >>> + NULL); >>> +} >>> + >>> +static void handle_midi_io(struct midi *midi, uint16_t value_handle) >>> +{ >>> + DBG("MIDI I/O handle: 0x%04x", value_handle); >>> + >>> + midi->midi_io_handle = value_handle; >>> + >>> + /* >>> + * The BLE-MIDI 1.0 spec specifies that The Central shall attempt to >>> + * read the MIDI I/O characteristic of the Peripheral right after >>> + * estrablhishing a connection with the accessory. >>> + */ >>> + if (!bt_gatt_client_read_value(midi->client, >>> + value_handle, >>> + midi_io_initial_read_cb, >>> + midi, >>> + NULL)) >>> + DBG("MIDI I/O: Failed to send request to read initial value"); >>> +} >>> + >>> +static void handle_characteristic(struct gatt_db_attribute *attr, >>> + gpointer user_data) >>> +{ >>> + struct midi *midi = user_data; >>> + uint16_t value_handle; >>> + bt_uuid_t uuid, midi_io_uuid; >>> + >>> + if (!gatt_db_attribute_get_char_data(attr, NULL, &value_handle, NULL, >>> + NULL, &uuid)) { >>> + error("Failed to obtain characteristic data"); >>> + return; >>> + } >>> + >>> + bt_string_to_uuid(&midi_io_uuid, MIDI_IO_UUID); >>> + >>> + if (bt_uuid_cmp(&midi_io_uuid, &uuid) == 0) >>> + handle_midi_io(midi, value_handle); >>> + else { >>> + char uuid_str[MAX_LEN_UUID_STR]; >>> + >>> + bt_uuid_to_string(&uuid, uuid_str, sizeof(uuid_str)); >>> + DBG("Unsupported characteristic: %s", uuid_str); >>> + } >>> +} >>> + >>> +static void foreach_midi_service(struct gatt_db_attribute *attr, >>> + gpointer user_data) >>> +{ >>> + struct midi *midi = user_data; >>> + >>> + gatt_db_service_foreach_char(attr, handle_characteristic, midi); >>> +} >>> + >>> +static int midi_device_probe(struct btd_service *service) >>> +{ >>> + struct btd_device *device = btd_service_get_device(service); >>> + struct midi *midi; >>> + char addr[18]; >>> + >>> + ba2str(device_get_address(device), addr); >>> + DBG("MIDI GATT Driver profile probe (%s)", addr); >>> + >>> + /* Ignore, if we were probed for this device already */ >>> + midi = btd_service_get_user_data(service); >>> + if (midi) { >>> + error("Profile probed twice for the same device!"); >>> + return -EADDRINUSE; >>> + } >>> + >>> + midi = g_new0(struct midi, 1); >>> + if (!midi) >>> + return -ENOMEM; >>> + >>> + midi->dev = btd_device_ref(device); >>> + >>> + btd_service_set_user_data(service, midi); >>> + >>> + return 0; >>> +} >>> + >>> +static void midi_device_remove(struct btd_service *service) >>> +{ >>> + struct btd_device *device = btd_service_get_device(service); >>> + struct midi *midi; >>> + char addr[18]; >>> + >>> + ba2str(device_get_address(device), addr); >>> + DBG("MIDI GATT Driver profile remove (%s)", addr); >>> + >>> + midi = btd_service_get_user_data(service); >>> + if (!midi) { >>> + error("MIDI Service not handled by profile"); >>> + return; >>> + } >>> + >>> + btd_device_unref(midi->dev); >>> + g_free(midi); >>> +} >>> + >>> +static int midi_accept(struct btd_service *service) >>> +{ >>> + struct btd_device *device = btd_service_get_device(service); >>> + struct gatt_db *db = btd_device_get_gatt_db(device); >>> + struct bt_gatt_client *client = btd_device_get_gatt_client(device); >>> + bt_uuid_t midi_uuid; >>> + struct pollfd pfd; >>> + struct midi *midi; >>> + char addr[18]; >>> + char device_name[MAX_NAME_LENGTH + 11]; /* 11 = " Bluetooth\0"*/ >>> + int err; >>> + snd_seq_client_info_t *info; >>> + >>> + ba2str(device_get_address(device), addr); >>> + DBG("MIDI GATT Driver profile accept (%s)", addr); >>> + >>> + midi = btd_service_get_user_data(service); >>> + if (!midi) { >>> + error("MIDI Service not handled by profile"); >>> + return -ENODEV; >>> + } >>> + >>> + /* Port Name */ >>> + memset(device_name, 0, sizeof(device_name)); >>> + if (device_name_known(device)) >>> + device_get_name(device, device_name, sizeof(device_name)); >>> + else >>> + strncpy(device_name, addr, sizeof(addr)); >>> + >>> + /* ALSA Sequencer Client and Port Setup */ >>> + err = snd_seq_open(&midi->seq_handle, "default", SND_SEQ_OPEN_DUPLEX, 0); >>> + if (err < 0) { >>> + error("Could not open ALSA Sequencer: %s (%d)", snd_strerror(err), err); >>> + return err; >>> + } >>> + >>> + err = snd_seq_nonblock(midi->seq_handle, SND_SEQ_NONBLOCK); >>> + if (err < 0) { >>> + error("Could not set nonblock mode: %s (%d)", snd_strerror(err), err); >>> + goto _err_handle; >>> + } >>> + >>> + err = snd_seq_set_client_name(midi->seq_handle, device_name); >>> + if (err < 0) { >>> + error("Could not configure ALSA client: %s (%d)", snd_strerror(err), err); >>> + goto _err_handle; >>> + } >>> + >>> + err = snd_seq_client_id(midi->seq_handle); >>> + if (err < 0) { >>> + error("Could retreive ALSA client: %s (%d)", snd_strerror(err), err); >>> + goto _err_handle; >>> + } >>> + midi->seq_client_id = err; >>> + >>> + err = snd_seq_create_simple_port(midi->seq_handle, strcat(device_name, " Bluetooth"), >>> + SND_SEQ_PORT_CAP_READ | >>> + SND_SEQ_PORT_CAP_WRITE | >>> + SND_SEQ_PORT_CAP_SUBS_READ | >>> + SND_SEQ_PORT_CAP_SUBS_WRITE, >>> + SND_SEQ_PORT_TYPE_MIDI_GENERIC | >>> + SND_SEQ_PORT_TYPE_HARDWARE); >>> + if (err < 0) { >>> + error("Could not create ALSA port: %s (%d)", snd_strerror(err), err); >>> + goto _err_handle; >>> + } >>> + midi->seq_port_id = err; >>> + >>> + snd_seq_client_info_alloca(&info); >>> + err = snd_seq_get_client_info(midi->seq_handle, info); >>> + if (err < 0) >>> + goto _err_port; >>> + >>> + /* list of relevant sequencer events */ >>> + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_NOTEOFF); >>> + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_NOTEON); >>> + 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_SYSEX); >>> + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_QFRAME); >>> + 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_TUNE_REQUEST); >>> + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CLOCK); >>> + 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_SENSING); >>> + snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_RESET); >>> + 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); >>> + >>> + err = snd_seq_set_client_info(midi->seq_handle, info); >>> + if (err < 0) >>> + goto _err_port; >>> + >>> + >>> + /* Input file descriptors */ >>> + snd_seq_poll_descriptors(midi->seq_handle, &pfd, 1, POLLIN); >>> + >>> + midi->io = io_new(pfd.fd); >>> + if (!midi->io) { >>> + error("Could not allocate I/O eventloop"); >>> + goto _err_port; >>> + } >>> + >>> + io_set_read_handler(midi->io, midi_write_cb, midi, NULL); >>> + >>> + midi->db = gatt_db_ref(db); >>> + midi->client = bt_gatt_client_ref(client); >>> + >>> + err = midi_read_init(&midi->midi_in); >>> + if (err < 0) { >>> + error("Could not initialise MIDI input parser"); >>> + goto _err_port; >>> + } >>> + >>> + err = midi_write_init(&midi->midi_out, bt_gatt_client_get_mtu(midi->client) - 3); >>> + if (err < 0) { >>> + error("Could not initialise MIDI output parser"); >>> + goto _err_midi; >>> + } >>> + >>> + bt_string_to_uuid(&midi_uuid, MIDI_UUID); >>> + gatt_db_foreach_service(db, &midi_uuid, foreach_midi_service, midi); >>> + >>> + btd_service_connecting_complete(service, 0); >>> + >>> + return 0; >>> + >>> +_err_midi: >>> + midi_read_free(&midi->midi_in); >>> + >>> +_err_port: >>> + snd_seq_delete_simple_port(midi->seq_handle, midi->seq_port_id); >>> + >>> +_err_handle: >>> + snd_seq_close(midi->seq_handle); >>> + midi->seq_handle = NULL; >>> + >>> + return err; >>> +} >>> + >>> +static int midi_disconnect(struct btd_service *service) >>> +{ >>> + struct btd_device *device = btd_service_get_device(service); >>> + struct midi *midi; >>> + char addr[18]; >>> + >>> + ba2str(device_get_address(device), addr); >>> + DBG("MIDI GATT Driver profile disconnect (%s)", addr); >>> + >>> + midi = btd_service_get_user_data(service); >>> + if (!midi) { >>> + error("MIDI Service not handled by profile"); >>> + return -ENODEV; >>> + } >>> + >>> + midi_read_free(&midi->midi_in); >>> + midi_write_free(&midi->midi_out); >>> + io_destroy(midi->io); >>> + snd_seq_delete_simple_port(midi->seq_handle, midi->seq_port_id); >>> + midi->seq_port_id = 0; >>> + snd_seq_close(midi->seq_handle); >>> + midi->seq_handle = NULL; >>> + >>> + /* Clean-up any old client/db */ >>> + bt_gatt_client_unregister_notify(midi->client, midi->io_cb_id); >>> + bt_gatt_client_unref(midi->client); >>> + gatt_db_unref(midi->db); >>> + >>> + btd_service_disconnecting_complete(service, 0); >>> + >>> + return 0; >>> +} >>> + >>> +static struct btd_profile midi_profile = { >>> + .name = "MIDI GATT Driver", >>> + .remote_uuid = MIDI_UUID, >>> + .priority = BTD_PROFILE_PRIORITY_HIGH, >>> + .auto_connect = TRUE, >>> + >>> + .device_probe = midi_device_probe, >>> + .device_remove = midi_device_remove, >>> + >>> + .accept = midi_accept, >>> + >>> + .disconnect = midi_disconnect, >>> +}; >>> + >>> +static int midi_init(void) >>> +{ >>> + return btd_profile_register(&midi_profile); >>> +} >>> + >>> +static void midi_exit(void) >>> +{ >>> + btd_profile_unregister(&midi_profile); >>> +} >>> + >>> +BLUETOOTH_PLUGIN_DEFINE(midi, VERSION, BLUETOOTH_PLUGIN_PRIORITY_HIGH, >>> + midi_init, midi_exit); > > Split the unit test into another patch, either together with the > library or as separate patch. Ok. > >>> diff --git a/unit/test-midi.c b/unit/test-midi.c >>> new file mode 100644 >>> index 000000000000..f786c767d7df >>> --- /dev/null >>> +++ b/unit/test-midi.c >>> @@ -0,0 +1,464 @@ >>> +/* >>> + * >>> + * BlueZ - Bluetooth protocol stack for Linux >>> + * >>> + * Copyright (C) 2015,2016 Felipe F. Tonello <eu@xxxxxxxxxxxxxxxxx> >>> + * Copyright (C) 2016 ROLI Ltd. >>> + * >>> + * This program is free software; you can redistribute it and/or modify >>> + * it under the terms of the GNU General Public License as published by >>> + * the Free Software Foundation; either version 2 of the License, or >>> + * (at your option) any later version. >>> + * >>> + * This program is distributed in the hope that it will be useful, >>> + * but WITHOUT ANY WARRANTY; without even the implied warranty of >>> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >>> + * GNU General Public License for more details. >>> + * >>> + * You should have received a copy of the GNU General Public License >>> + * along with this program; if not, write to the Free Software >>> + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA >>> + * >>> + * >>> + */ >>> + >>> +#ifdef HAVE_CONFIG_H >>> +#include <config.h> >>> +#endif >>> + >>> +#define NUM_WRITE_TESTS 10 >>> + >>> +#include "src/shared/tester.h" >>> +#include "profiles/midi/libmidi.h" >>> + >>> +struct ble_midi_packet { >>> + const guint8 *data; >>> + gsize size; >>> +}; >>> + >>> +#define BLE_MIDI_PACKET_INIT(_packet) \ >>> + { \ >>> + .data = _packet, \ >>> + .size = sizeof(_packet), \ >>> + } >>> + >>> +struct midi_read_test { >>> + const struct ble_midi_packet *ble_packet; >>> + gsize ble_packet_size; >>> + const snd_seq_event_t *event; >>> + gsize event_size; >>> +}; >>> + >>> +#define BLE_READ_TEST_INIT(_ble_packet, _event) \ >>> + { \ >>> + .ble_packet = _ble_packet, \ >>> + .ble_packet_size = G_N_ELEMENTS(_ble_packet), \ >>> + .event = _event, \ >>> + .event_size = G_N_ELEMENTS(_event), \ >>> + } >>> + >>> +struct midi_write_test { >>> + const snd_seq_event_t *event; >>> + gsize event_size; >>> +}; >>> + >>> +#define BLE_WRITE_TEST_INIT(_event) \ >>> + { \ >>> + .event = _event, \ >>> + .event_size = G_N_ELEMENTS(_event), \ >>> + } >>> + >>> + >>> +#define NOTE_EVENT(_event, _channel, _note, _velocity) \ >>> + { \ >>> + .type = SND_SEQ_EVENT_##_event, \ >>> + .data = { \ >>> + .note = { \ >>> + .channel = (_channel), \ >>> + .note = (_note), \ >>> + .velocity = (_velocity), \ >>> + }, \ >>> + }, \ >>> + } >>> + >>> +#define CONTROL_EVENT(_event, _channel, _value, _param) \ >>> + { \ >>> + .type = SND_SEQ_EVENT_##_event, \ >>> + .data = { \ >>> + .control = { \ >>> + .channel = (_channel), \ >>> + .value = (_value), \ >>> + .param = (_param), \ >>> + }, \ >>> + }, \ >>> + } >>> + >>> +#define SYSEX_EVENT(_message) \ >>> + { \ >>> + .type = SND_SEQ_EVENT_SYSEX, \ >>> + .data = { \ >>> + .ext = { \ >>> + .ptr = (void *)_message, \ >>> + .len = sizeof(_message), \ >>> + }, \ >>> + }, \ >>> + } >>> + >>> +/* Multiple messages in one packet */ >>> +static const guint8 packet1_1[] = { >>> + 0xa6, 0x88, 0xe8, 0x00, 0x40, 0x88, 0xb8, 0x4a, >>> + 0x3f, 0x88, 0x98, 0x3e, 0x0e >>> +}; >>> + >>> +/* Several one message per packet */ >>> +static const guint8 packet1_2[] = { >>> + 0xa6, 0xaa, 0xd8, 0x71 >>> +}; >>> + >>> +static const guint8 packet1_3[] = { >>> + 0xa6, 0xb7, 0xb8, 0x4a, 0x43 >>> +}; >>> + >>> +/* This message contains a running status message */ >>> +static const guint8 packet1_4[] = { >>> + 0xa6, 0xc4, 0xe8, 0x7e, 0x3f, 0x7d, 0x3f, 0xc4, >>> + 0x7c, 0x3f >>> +}; >>> + >>> +/* This message contain a running status message misplaced */ >>> +static const guint8 packet1_5[] = { >>> + 0xa6, 0xd9, 0x3e, 0x00, 0x88, 0x3e, 0x00 >>> +}; >>> + >>> +static const struct ble_midi_packet packet1[] = { >>> + BLE_MIDI_PACKET_INIT(packet1_1), >>> + BLE_MIDI_PACKET_INIT(packet1_2), >>> + BLE_MIDI_PACKET_INIT(packet1_3), >>> + BLE_MIDI_PACKET_INIT(packet1_4), >>> + BLE_MIDI_PACKET_INIT(packet1_5), >>> +}; >>> + >>> +static const snd_seq_event_t event1[] = { >>> + CONTROL_EVENT(PITCHBEND, 8, 0, 0), /* Pitch Bend */ >>> + CONTROL_EVENT(CONTROLLER, 8, 63, 74), /* Control Change */ >>> + NOTE_EVENT(NOTEON, 8, 62, 14), /* Note On */ >>> + CONTROL_EVENT(CHANPRESS, 8, 113, 0), /* Channel Aftertouch */ >>> + CONTROL_EVENT(CONTROLLER, 8, 67, 74), /* Control Change*/ >>> + CONTROL_EVENT(PITCHBEND, 8, -2, 0), /* Pitch Bend */ >>> + CONTROL_EVENT(PITCHBEND, 8, -3, 0), /* Pitch Bend */ >>> + CONTROL_EVENT(PITCHBEND, 8, -4, 0), /* Pitch Bend */ >>> + NOTE_EVENT(NOTEOFF, 8, 62, 0), /* Note Off */ >>> +}; >>> + >>> +static const struct midi_read_test midi1 = BLE_READ_TEST_INIT(packet1, event1); >>> + >>> +/* Basic SysEx in one packet */ >>> +static const guint8 packet2_1[] = { >>> + 0xa6, 0xda, 0xf0, 0x01, 0x02, 0x03, 0xda, 0xf7 >>> +}; >>> + >>> +/* SysEx across two packets */ >>> +static const guint8 packet2_2[] = { >>> + 0xa6, 0xda, 0xf0, 0x01, 0x02, 0x03, 0x04, 0x05 >>> +}; >>> + >>> +static const guint8 packet2_3[] = { >>> + 0xa6, 0x06, 0x07, 0x08, 0x09, 0x0a, 0xdb, 0xf7 >>> +}; >>> + >>> +/* SysEx across multiple packets */ >>> +static const guint8 packet2_4[] = { >>> + 0xa6, 0xda, 0xf0, 0x01, 0x02, 0x03, 0x04, 0x05 >>> +}; >>> + >>> +static const guint8 packet2_5[] = { >>> + 0xa6, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c >>> +}; >>> +static const guint8 packet2_6[] = { >>> + 0xa6, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13 >>> +}; >>> + >>> +static const guint8 packet2_7[] = { >>> + 0xa6, 0x14, 0x15, 0x16, 0x17, 0x18, 0xdb, 0xf7 >>> +}; >>> + >>> +/* Two SysEx interleaved in two packets */ >>> +static const guint8 packet2_8[] = { >>> + 0xa6, 0xda, 0xf0, 0x01, 0x02, 0x03, 0xda, 0xf7, >>> + 0xda, 0xf0 >>> +}; >>> + >>> +static const guint8 packet2_9[] = { >>> + 0xa6, 0x06, 0x07, 0x08, 0x09, 0x0a, 0xdb, 0xf7 >>> +}; >>> + >>> + >>> +static const struct ble_midi_packet packet2[] = { >>> + BLE_MIDI_PACKET_INIT(packet2_1), >>> + BLE_MIDI_PACKET_INIT(packet2_2), >>> + BLE_MIDI_PACKET_INIT(packet2_3), >>> + BLE_MIDI_PACKET_INIT(packet2_4), >>> + BLE_MIDI_PACKET_INIT(packet2_5), >>> + BLE_MIDI_PACKET_INIT(packet2_6), >>> + BLE_MIDI_PACKET_INIT(packet2_7), >>> + BLE_MIDI_PACKET_INIT(packet2_8), >>> + BLE_MIDI_PACKET_INIT(packet2_9), >>> +}; >>> + >>> +static const guint8 sysex2_1[] = { >>> + 0xf0, 0x01, 0x02, 0x03, 0xf7 >>> +}; >>> + >>> +static const guint8 sysex2_2[] = { >>> + 0xf0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, >>> + 0x08, 0x09, 0x0a, 0xf7 >>> +}; >>> + >>> +static const guint8 sysex2_3[] = { >>> + 0xf0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, >>> + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, >>> + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, >>> + 0x18, 0xf7 >>> +}; >>> + >>> +static const guint8 sysex2_4[] = { >>> + 0xf0, 0x01, 0x02, 0x03, 0xf7 >>> +}; >>> + >>> +static const guint8 sysex2_5[] = { >>> + 0xf0, 0x06, 0x07, 0x08, 0x09, 0x0a, 0xf7 >>> +}; >>> + >>> +static const snd_seq_event_t event2[] = { >>> + SYSEX_EVENT(sysex2_1), >>> + SYSEX_EVENT(sysex2_2), >>> + SYSEX_EVENT(sysex2_3), >>> + SYSEX_EVENT(sysex2_4), >>> + SYSEX_EVENT(sysex2_5), >>> +}; >>> + >>> +static const struct midi_read_test midi2 = BLE_READ_TEST_INIT(packet2, event2); >>> + >>> +static void compare_events(const snd_seq_event_t *ev1, >>> + const snd_seq_event_t *ev2) >>> +{ >>> + g_assert_cmpint(ev1->type, ==, ev2->type); >>> + >>> + switch (ev1->type) { >>> + case SND_SEQ_EVENT_NOTEON: >>> + case SND_SEQ_EVENT_NOTEOFF: >>> + case SND_SEQ_EVENT_KEYPRESS: >>> + g_assert_cmpint(ev1->data.note.channel, >>> + ==, >>> + ev2->data.note.channel); >>> + g_assert_cmpint(ev1->data.note.note, >>> + ==, >>> + ev2->data.note.note); >>> + g_assert_cmpint(ev1->data.note.velocity, >>> + ==, >>> + ev2->data.note.velocity); >>> + break; >>> + case SND_SEQ_EVENT_CONTROLLER: >>> + g_assert_cmpint(ev1->data.control.param, >>> + ==, >>> + ev2->data.control.param); >>> + case SND_SEQ_EVENT_PITCHBEND: >>> + case SND_SEQ_EVENT_CHANPRESS: >>> + case SND_SEQ_EVENT_PGMCHANGE: >>> + g_assert_cmpint(ev1->data.control.channel, >>> + ==, >>> + ev2->data.control.channel); >>> + g_assert_cmpint(ev1->data.control.value, >>> + ==, >>> + ev2->data.control.value); >>> + break; >>> + case SND_SEQ_EVENT_SYSEX: >>> + g_assert_cmpmem(ev1->data.ext.ptr, ev1->data.ext.len, >>> + ev2->data.ext.ptr, ev2->data.ext.len); >>> + break; >>> + default: >>> + g_assert_not_reached(); >>> + } >>> +} >>> + >>> +static void test_midi_reader(gconstpointer data) >>> +{ >>> + const struct midi_read_test *midi_test = data; >>> + struct midi_read_parser midi; >>> + int err; >>> + gsize i; /* ble_packet counter */ >>> + gsize j; /* ble_packet length counter */ >>> + gsize k = 0; /* event counter */ >>> + >>> + err = midi_read_init(&midi); >>> + g_assert_cmpint(err, ==, 0); >>> + >>> + for (i = 0; i < midi_test->ble_packet_size; i++) { >>> + const gsize length = midi_test->ble_packet[i].size; >>> + j = 0; >>> + midi_read_reset(&midi); >>> + while (j < length) { >>> + snd_seq_event_t ev; >>> + const snd_seq_event_t *ev_expect = &midi_test->event[k]; >>> + gsize count; >>> + >>> + g_assert_cmpint(k, <, midi_test->event_size); >>> + >>> + snd_seq_ev_clear(&ev); >>> + >>> + count = midi_read_raw(&midi, >>> + midi_test->ble_packet[i].data + j, >>> + length - j, >>> + &ev); >>> + >>> + g_assert_cmpuint(count, >, 0); >>> + >>> + if (ev.type == SND_SEQ_EVENT_NONE) >>> + goto _continue_loop; >>> + else >>> + k++; >>> + >>> + compare_events(ev_expect, &ev); >>> + >>> + _continue_loop: >>> + j += count; >>> + } >>> + } >>> + >>> + midi_read_free(&midi); >>> + >>> + tester_test_passed(); >>> +} >>> + >>> +static const snd_seq_event_t event3[] = { >>> + CONTROL_EVENT(PITCHBEND, 8, 0, 0), /* Pitch Bend */ >>> + CONTROL_EVENT(CONTROLLER, 8, 63, 74), /* Control Change */ >>> + NOTE_EVENT(NOTEON, 8, 62, 14), /* Note On */ >>> + CONTROL_EVENT(CHANPRESS, 8, 113, 0), /* Channel Aftertouch */ >>> + CONTROL_EVENT(CONTROLLER, 8, 67, 74), /* Control Change*/ >>> + CONTROL_EVENT(PITCHBEND, 8, -2, 0), /* Pitch Bend */ >>> + CONTROL_EVENT(PITCHBEND, 8, -3, 0), /* Pitch Bend */ >>> + CONTROL_EVENT(PITCHBEND, 8, -4, 0), /* Pitch Bend */ >>> + NOTE_EVENT(NOTEOFF, 8, 62, 0), /* Note Off */ >>> +}; >>> + >>> +static const struct midi_write_test midi3 = BLE_WRITE_TEST_INIT(event3); >>> + >>> +static const guint8 sysex4_1[] = { >>> + 0xf0, 0x01, 0x02, 0x03, 0xf7 >>> +}; >>> + >>> +static const guint8 sysex4_2[] = { >>> + 0xf0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, >>> + 0x08, 0x09, 0x0a, 0xf7 >>> +}; >>> + >>> +static const guint8 sysex4_3[] = { >>> + 0xf0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, >>> + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, >>> + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, >>> + 0x18, 0xf7 >>> +}; >>> + >>> +static const guint8 sysex4_4[] = { >>> + 0xf0, 0x01, 0x02, 0x03, 0xf7 >>> +}; >>> + >>> +static const guint8 sysex4_5[] = { >>> + 0xf0, 0x06, 0x07, 0x08, 0x09, 0x0a, 0xf7 >>> +}; >>> + >>> +static const snd_seq_event_t event4[] = { >>> + SYSEX_EVENT(sysex4_1), >>> + SYSEX_EVENT(sysex4_2), >>> + SYSEX_EVENT(sysex4_3), >>> + SYSEX_EVENT(sysex4_4), >>> + SYSEX_EVENT(sysex4_5), >>> +}; >>> + >>> +static const struct midi_write_test midi4 = BLE_WRITE_TEST_INIT(event4); >>> + >>> +static void test_midi_writer(gconstpointer data) >>> +{ >>> + const struct midi_write_test *midi_test = data; >>> + struct midi_write_parser midi_out; >>> + struct midi_read_parser midi_in; >>> + gsize i; /* event counter */ >>> + gsize j; /* test counter */ >>> + static struct midi_data { >>> + gsize events_tested; >>> + const struct midi_write_test *midi_test; >>> + struct midi_read_parser *midi_in; >>> + } midi_data; >>> + >>> + void compare_events_cb(const guint8 *data, guint size, gpointer user_data) { >>> + struct midi_data *midi_data = user_data; >>> + const struct midi_write_test *midi_test = midi_data->midi_test; >>> + struct midi_read_parser *midi_in = midi_data->midi_in; >>> + gsize i = 0; >>> + >>> + midi_read_reset(midi_in); >>> + >>> + while (i < size) { >>> + snd_seq_event_t ev; >>> + gsize count; >>> + >>> + snd_seq_ev_clear(&ev); >>> + >>> + count = midi_read_raw(midi_in, data + i, >>> + size - i, &ev); >>> + >>> + g_assert_cmpuint(count, >, 0); >>> + >>> + if (ev.type != SND_SEQ_EVENT_NONE){ >>> + g_assert_cmpint(midi_data->events_tested, >>> + <, >>> + midi_test->event_size); >>> + compare_events(&midi_test->event[midi_data->events_tested], >>> + &ev); >>> + midi_data->events_tested++; >>> + } >>> + >>> + i += count; >>> + } >>> + }; >>> + >>> + midi_read_init(&midi_in); >>> + >>> + for (j = 0; j < NUM_WRITE_TESTS; j++) { >>> + >>> + /* Range of test for different MTU sizes. The spec specifies >>> + sizes of packet as MTU - 3 */ >>> + midi_write_init(&midi_out, g_random_int_range(5, 40)); >>> + >>> + midi_data.events_tested = 0; >>> + midi_data.midi_test = midi_test; >>> + midi_data.midi_in = &midi_in; >>> + >>> + for (i = 0; i < midi_test->event_size; i++) >>> + midi_read_ev(&midi_out, &midi_test->event[i], >>> + compare_events_cb, &midi_data); >>> + >>> + if (midi_write_has_data(&midi_out)) >>> + compare_events_cb(midi_write_data(&midi_out), >>> + midi_write_data_size(&midi_out), >>> + &midi_data); >>> + >>> + midi_write_free(&midi_out); >>> + } >>> + midi_read_free(&midi_in); >>> + >>> + tester_test_passed(); >>> +} >>> + >>> +int main(int argc, char *argv[]) >>> +{ >>> + tester_init(&argc, &argv); >>> + >>> + tester_add("/midi/1", &midi1, NULL, test_midi_reader, NULL); >>> + tester_add("/midi/2", &midi2, NULL, test_midi_reader, NULL); >>> + tester_add("/midi/3", &midi3, NULL, test_midi_writer, NULL); >>> + tester_add("/midi/4", &midi4, NULL, test_midi_writer, NULL); >>> + >>> + return tester_run(); >>> +} -- Felipe
Attachment:
0x92698E6A.asc
Description: application/pgp-keys