Skip to content

Commit

Permalink
Encoder for BLE-MIDI with conformance tests
Browse files Browse the repository at this point in the history
  • Loading branch information
arkq committed Dec 3, 2023
1 parent 3f6fc51 commit f8b5511
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 0 deletions.
81 changes: 81 additions & 0 deletions src/ble-midi.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <sys/param.h>

#include "shared/log.h"
#include "shared/rt.h"

/**
* Determine length of the MIDI message based on the status byte. */
Expand Down Expand Up @@ -236,3 +239,81 @@ int ble_midi_decode(struct ble_midi_dec *bmd, const uint8_t *data, size_t len) {
bmd->current_len = 0;
return -1;
}

/**
* Encode BLE-MIDI packet.
*
* It is possible that a single MIDI system exclusive message will not fit
* into the MTU of the BLE link. In such case, this function will return 1
* and the caller should call this function again with the same MIDI message.
* The encoder structure should not be modified between consecutive calls.
*
* @param bme BLE-MIDI encoder structure.
* @param data Single MIDI message.
* @param len Length of the MIDI message data.
* @return On success, this function returns 0. If the BLE-MIDI packet has to
* be split into multiple packets, 1 is returned. On error, -1 is returned. */
int ble_midi_encode(struct ble_midi_enc *enc, const uint8_t *data, size_t len) {

const bool is_sys = data[0] == 0xF0;
bool is_sys_continue = false;
size_t transfer_len = len;

/* Check if the MTU is at least 5 bytes (header + timestamp + MIDI message)
* and does not exceed the buffer size of the encoder structure. */
if (enc->mtu < 5 || enc->mtu > sizeof(enc->buffer)) {
error("Invalid BLE-MIDI encoder MTU: %zu", enc->mtu);
return errno = EINVAL, -1;
}

/* Check if the message will fit within the MTU. In case of consecutive
* encode calls, this check is off by one, but we can live with that. This
* check does not apply to the system exclusive messages. */
if (enc->len + 2 + len > enc->mtu && !is_sys)
return errno = EMSGSIZE, -1;

/* Check if the message is a system exclusive message
* and if it is a continuation call. */
if (is_sys && enc->len == enc->mtu) {
is_sys_continue = true;
enc->len = 0;
}

struct timespec now;
gettimestamp(&now);
enc->ts = now.tv_sec * 1000 + now.tv_nsec / 1000000;

if (enc->len == 0) {
/* Construct the BLE-MIDI header with the most significant
* 6 bits of the 13-bits milliseconds timestamp. */
enc->buffer[enc->len++] = 0x80 | ((enc->ts >> 7) & 0x3F);
}

if (!is_sys_continue) {
/* Add the timestamp byte with the least significant 7 bits
* of the timestamp. */
enc->buffer[enc->len++] = 0x80 | (enc->ts & 0x7F);
}

if (is_sys)
/* Calculate the number of bytes that we can transfer. */
transfer_len = MIN(len - enc->current_len, enc->mtu - enc->len);

memcpy(&enc->buffer[enc->len], &data[enc->current_len], transfer_len);
enc->len += transfer_len;

if (is_sys) {
if ((enc->current_len += transfer_len) != len)
return 1;
enc->current_len = 0;
}

return 0;
}

/**
* Set BLE-MIDI encoder MTU. */
int ble_midi_encode_set_mtu(struct ble_midi_enc *bme, size_t mtu) {
bme->mtu = MIN(mtu, sizeof(bme->buffer));
return 0;
}
20 changes: 20 additions & 0 deletions src/ble-midi.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,26 @@ struct ble_midi_dec {

};

struct ble_midi_enc {

/* The MTU of the BLE link. This structure member shall be set before
* calling the ble_midi_encode() function. */
size_t mtu;

/* encoded BLE-MIDI message */
uint8_t buffer[512];
/* length of the encoded message */
size_t len;

/* current timestamp */
unsigned int ts;
/* current encoding position */
size_t current_len;

};

int ble_midi_decode(struct ble_midi_dec *bmd, const uint8_t *data, size_t len);
int ble_midi_encode(struct ble_midi_enc *bme, const uint8_t *data, size_t len);
int ble_midi_encode_set_mtu(struct ble_midi_enc *bme, size_t mtu);

#endif
110 changes: 110 additions & 0 deletions test/test-ble-midi.c
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,110 @@ CK_START_TEST(test_ble_midi_decode_multiple_running_status) {

} CK_END_TEST

CK_START_TEST(test_ble_midi_encode_no_mtu) {

const uint8_t midi[] = { 0x90, 0x40, 0x7f };

struct ble_midi_enc bme = { 0 };
ck_assert_int_eq(ble_midi_encode(&bme, midi, sizeof(midi)), -1);
ck_assert_uint_eq(errno, EINVAL);

} CK_END_TEST

CK_START_TEST(test_ble_midi_encode_single) {

const uint8_t midi[] = { 0x90, 0x40, 0x7f };

struct ble_midi_enc bme = { 0 };
ble_midi_encode_set_mtu(&bme, 24);

ck_assert_int_eq(ble_midi_encode(&bme, midi, sizeof(midi)), 0);

/* header (1 byte) + timestamp (1 byte) + MIDI message (3 bytes) */
ck_assert_uint_eq(bme.len, 1 + 1 + sizeof(midi));
ck_assert_uint_eq(bme.buffer[0] >> 6, 0x02);
ck_assert_uint_eq(bme.buffer[1] & 0x80, 0x80);
ck_assert_mem_eq(&bme.buffer[2], midi, sizeof(midi));

} CK_END_TEST

CK_START_TEST(test_ble_midi_encode_multiple) {

const uint8_t midi1[] = { 0xC0, 0x01 };
const uint8_t midi2[] = { 0x90, 0x40, 0x7f };
const uint8_t midi3[] = { 0xF8 };

struct ble_midi_enc bme = { 0 };
ble_midi_encode_set_mtu(&bme, 24);

ck_assert_int_eq(ble_midi_encode(&bme, midi1, sizeof(midi1)), 0);
ck_assert_int_eq(ble_midi_encode(&bme, midi2, sizeof(midi2)), 0);
ck_assert_int_eq(ble_midi_encode(&bme, midi3, sizeof(midi3)), 0);

/* The length of the encoded data should be equal to the sum of the
* lengths of the encoded MIDI messages plus the length of the header
* (1 byte) and the timestamp (1 byte) bytes. */
ck_assert_uint_eq(bme.len, 4 + sizeof(midi1) + sizeof(midi2) + sizeof(midi3));

ck_assert_uint_eq(bme.buffer[0] >> 6, 0x02);

ck_assert_uint_eq(bme.buffer[1] & 0x80, 0x80);
ck_assert_mem_eq(&bme.buffer[2], midi1, sizeof(midi1));

ck_assert_uint_eq(bme.buffer[4] & 0x80, 0x80);
ck_assert_mem_eq(&bme.buffer[5], midi2, sizeof(midi2));

ck_assert_uint_eq(bme.buffer[8] & 0x80, 0x80);
ck_assert_mem_eq(&bme.buffer[9], midi3, sizeof(midi3));

} CK_END_TEST

CK_START_TEST(test_ble_midi_encode_multiple_too_long) {

const uint8_t midi1[] = { 0x80, 0x40, 0x7f };
const uint8_t midi2[] = { 0x90, 0x40, 0x7f };

struct ble_midi_enc bme = { 0 };
ble_midi_encode_set_mtu(&bme, 8);

ck_assert_int_eq(ble_midi_encode(&bme, midi1, sizeof(midi1)), 0);
ck_assert_int_eq(ble_midi_encode(&bme, midi2, sizeof(midi2)), -1);
ck_assert_uint_eq(errno, EMSGSIZE);

/* Messages up to the MTU should be encoded properly. */
ck_assert_uint_eq(bme.len, 2 + sizeof(midi1));

} CK_END_TEST

CK_START_TEST(test_ble_midi_encode_system_exclusive) {

const uint8_t midi1[] = { 0xF0, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 };
const uint8_t midi2[] = { 0xF7 };

struct ble_midi_enc bme = { 0 };
ble_midi_encode_set_mtu(&bme, 8);

ck_assert_int_eq(ble_midi_encode(&bme, midi1, sizeof(midi1)), 1);

ck_assert_uint_eq(bme.len, 8 /* MTU */);
ck_assert_uint_eq(bme.buffer[0] >> 6, 0x02);
ck_assert_uint_eq(bme.buffer[1] & 0x80, 0x80);
ck_assert_mem_eq(&bme.buffer[2], midi1, 6 /* MTU - 2 */);

ck_assert_int_eq(ble_midi_encode(&bme, midi1, sizeof(midi1)), 0);
ck_assert_int_eq(ble_midi_encode(&bme, midi2, sizeof(midi2)), 0);

/* The continuation of the system exclusive message shall not contain
* the timestamp byte after the header, but the end of the exclusive
* message shall contain the timestamp byte. */
ck_assert_uint_eq(bme.len, 1 + (sizeof(midi1) - 6) + 1 + sizeof(midi2));
ck_assert_uint_eq(bme.buffer[0] >> 6, 0x02);
ck_assert_mem_eq(&bme.buffer[1], &midi1[6], sizeof(midi1) - 6);
ck_assert_uint_eq(bme.buffer[3] & 0x80, 0x80);
ck_assert_mem_eq(&bme.buffer[4], midi2, sizeof(midi2));

} CK_END_TEST

int main(void) {

Suite *s = suite_create(__FILE__);
Expand All @@ -374,6 +478,12 @@ int main(void) {
tcase_add_test(tc, test_ble_midi_decode_single_running_status_with_common);
tcase_add_test(tc, test_ble_midi_decode_multiple_running_status);

tcase_add_test(tc, test_ble_midi_encode_no_mtu);
tcase_add_test(tc, test_ble_midi_encode_single);
tcase_add_test(tc, test_ble_midi_encode_multiple);
tcase_add_test(tc, test_ble_midi_encode_multiple_too_long);
tcase_add_test(tc, test_ble_midi_encode_system_exclusive);

srunner_run_all(sr, CK_ENV);
int nf = srunner_ntests_failed(sr);
srunner_free(sr);
Expand Down

0 comments on commit f8b5511

Please sign in to comment.