Return-Path: From: "Felipe F. Tonello" To: linux-bluetooth@vger.kernel.org Subject: [PATCH BlueZ] profiles/midi: Added MIDI over BLE profile implementation Date: Fri, 18 Nov 2016 14:59:44 +0000 Message-Id: <20161118145944.22266-1-eu@felipetonello.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Sender: linux-bluetooth-owner@vger.kernel.org List-ID: This plugin implements the Central roli of MIDI over Bluetooth Low-Energy (BLE-MIDI) 1.0 specification as published by MMA in November/2015. It was implmemented as a bluez plugin because of latency requirements of MIDI. There are still room for improvements on this regard. Since all parsing and state-machine code is in libmidi.[hc] it should be simple to implement 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 I will still maintain a personal branch on my github[1] so others can contribute there and I can test before sending to bluez. This patch adds ALSA dependency since there is no other way of doing it, sorry for it. :/ But this can be easily disabled with --disable-alsa configure flag. I have tested on a normal laptop Arch-linux (x86_64) and a Raspberry Pi 2 (ARM A8) and it works very well. As I mentioned, the latency can always be improved. NOTE: 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. OBS: 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/ --- Makefile.am | 13 +- Makefile.plugins | 8 + README | 6 + configure.ac | 10 + profiles/midi/libmidi.c | 391 ++++++++++++++++++++++++++++++++++++++ profiles/midi/libmidi.h | 140 ++++++++++++++ profiles/midi/midi.c | 491 ++++++++++++++++++++++++++++++++++++++++++++++++ unit/test-midi.c | 464 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1522 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..c077b2568d5d 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 ALSA +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..3600f113304c 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 ALSA +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/README b/README index 3ee367092137..e582f1fadfb0 100644 --- a/README +++ b/README @@ -13,6 +13,12 @@ Read more about the project at: * http://blog.felipetonello.com/2015/12/14/midi-over-bluetooth-low-energy-on-linux/ * https://www.marc.info/?l=linux-bluetooth&m=144368891120651&w=4 +This branch[1] is under heavy development, so don't assume that things will not +break or the histry log will not change, because it will! + +The idea is that most MIDI related commits will be squashed and cleaned-up. + +[1] https://github.com/ftonello/bluez/ Compilation and installation ============================ diff --git a/configure.ac b/configure.ac index 768a20cc5b2b..ffe0dd61c4e5 100644 --- a/configure.ac +++ b/configure.ac @@ -210,8 +210,18 @@ AM_CONDITIONAL(HID2HCI, test "${enable_tools}" != "no" && AC_ARG_ENABLE(cups, AC_HELP_STRING([--disable-cups], [disable CUPS printer support]), [enable_cups=${enableval}]) +if (test "${enable_alsa}" != "no"); then + PKG_CHECK_MODULES(ALSA, alsa, dummy=yes, + AC_MSG_ERROR(ALSA lib is required)) + AC_SUBST(ALSA_CFLAGS) + AC_SUBST(ALSA_LIBS) +fi AM_CONDITIONAL(CUPS, test "${enable_cups}" != "no") +AC_ARG_ENABLE(alsa, AC_HELP_STRING([--disable-alsa], + [disable ALSA MIDI support]), [enable_alsa=${enableval}]) +AM_CONDITIONAL(ALSA, test "${enable_alsa}" != "no") + 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..970fe2b1a300 --- /dev/null +++ b/profiles/midi/libmidi.c @@ -0,0 +1,391 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2015,2016 Felipe F. Tonello + * 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 + * + * + */ + +#define _GNU_SOURCE + +#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) + + if (GET_AVAILABLE_SIZE == 0) { + write_cb(parser->midi_stream->data, parser->midi_stream->len, + user_data); + 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)]) + + /* 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 timestamp) +{ + int delta_timestamp; + int delta_rtime; + gint64 rtime_current; + + 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) { + + guint8 time_low_tmp = data[i] & 0x7F; + + /* time_low overwflow results on time_high to increment by one */ + if (parser->timestamp_low > time_low_tmp) + parser->timestamp_high++; + + parser->timestamp_low = time_low_tmp; + + update_ev_timestamp(parser, ev, (parser->timestamp_high << 7) | time_low); + + /* 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, + (parser->timestamp_high << 7) | 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, + (parser->timestamp_high << 7) | 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; +} 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 + * 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 +#include + +#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 */ +}; + +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..403fe5f02191 --- /dev/null +++ b/profiles/midi/midi.c @@ -0,0 +1,491 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2015,2016 Felipe F. Tonello + * 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 +#endif + +#include +#include +#include +#include + +#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 Centrail 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 port_name[MAX_NAME_LENGTH]; + 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(port_name, 0, sizeof(port_name)); + if (device_name_known(device)) + device_get_name(device, port_name, sizeof(port_name)); + else + strncpy(port_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, port_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, port_name, + 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); 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 + * 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 +#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(); +} -- 2.10.2