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. >> 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. >> 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. >> + >> + 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. >> + 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 >> + >> + /* 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. >> 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? >> +}; >> + >> +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. >> 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 -- Luiz Augusto von Dentz -- To unsubscribe from this list: send the line "unsubscribe linux-bluetooth" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html