diff options
Diffstat (limited to 'backends')
| -rw-r--r-- | backends/Makefile | 8 | ||||
| -rw-r--r-- | backends/rtpmidi.c | 691 | ||||
| -rw-r--r-- | backends/rtpmidi.h | 124 | ||||
| -rw-r--r-- | backends/rtpmidi.md | 87 | 
4 files changed, 908 insertions, 2 deletions
diff --git a/backends/Makefile b/backends/Makefile index 656e6b6..f0d5c3e 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@  .PHONY: all clean full  LINUX_BACKENDS = midi.so evdev.so -WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll rtpmidi.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so rtpmidi.so  OPTIONAL_BACKENDS = ola.so  BACKEND_LIB = libmmbackend.o @@ -42,6 +42,10 @@ maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)  maweb.dll: LDLIBS += -lws2_32  maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL +rtpmidi.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +rtpmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +rtpmidi.dll: LDLIBS += -lws2_32 +  winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)  winmidi.dll: LDLIBS += -lwinmm -lws2_32 diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c new file mode 100644 index 0000000..719f823 --- /dev/null +++ b/backends/rtpmidi.c @@ -0,0 +1,691 @@ +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <ctype.h> + +#include "libmmbackend.h" +#include "rtpmidi.h" + +#define BACKEND_NAME "rtpmidi" + +static struct /*_rtpmidi_global*/ { +	int mdns_fd; +	char* mdns_name; +	uint8_t detect; + +	size_t announces; +	rtpmidi_announce* announce; +} cfg = { +	.mdns_fd = -1, +	.mdns_name = NULL, +	.detect = 0, +	.announces = 0, +	.announce = NULL +}; + +MM_PLUGIN_API int init(){ +	backend rtpmidi = { +		.name = BACKEND_NAME, +		.conf = rtpmidi_configure, +		.create = rtpmidi_instance, +		.conf_instance = rtpmidi_configure_instance, +		.channel = rtpmidi_channel, +		.handle = rtpmidi_set, +		.process = rtpmidi_handle, +		.start = rtpmidi_start, +		.shutdown = rtpmidi_shutdown +	}; + +	if(sizeof(rtpmidi_channel_ident) != sizeof(uint64_t)){ +		fprintf(stderr, "rtpmidi channel identification union out of bounds\n"); +		return 1; +	} + +	if(mm_backend_register(rtpmidi)){ +		fprintf(stderr, "Failed to register rtpmidi backend\n"); +		return 1; +	} + +	return 0; +} + +static int rtpmidi_configure(char* option, char* value){ +	char* host = NULL, *port = NULL; + +	if(!strcmp(option, "mdns-name")){ +		if(cfg.mdns_name){ +			fprintf(stderr, "Duplicate mdns-name assignment\n"); +			return 1; +		} + +		cfg.mdns_name = strdup(value); +		if(!cfg.mdns_name){ +			fprintf(stderr, "Failed to allocate memory\n"); +			return 1; +		} +		return 0; +	} +	else if(!strcmp(option, "mdns-bind")){ +		if(cfg.mdns_fd >= 0){ +			fprintf(stderr, "Only one mDNS discovery bind is supported\n"); +			return 1; +		} + +		mmbackend_parse_hostspec(value, &host, &port); + +		if(!host){ +			fprintf(stderr, "Not a valid mDNS bind address: %s\n", value); +			return 1; +		} + +		cfg.mdns_fd = mmbackend_socket(host, (port ? port : RTPMIDI_MDNS_PORT), SOCK_DGRAM, 1, 1); +		if(cfg.mdns_fd < 0){ +			fprintf(stderr, "Failed to bind mDNS interface: %s\n", value); +			return 1; +		} +		return 0; +	} +	else if(!strcmp(option, "detect")){ +		cfg.detect = 0; +		if(!strcmp(value, "on")){ +			cfg.detect = 1; +		} +		return 0; +	} + +	fprintf(stderr, "Unknown rtpmidi backend option %s\n", option); +	return 1; +} + +static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* port){ +	struct sockaddr_storage sock_addr = { +		0 +	}; +	socklen_t sock_len = sizeof(sock_addr); +	char control_port[32]; + +	//bind to random port if none supplied +	data->fd = mmbackend_socket(host, port ? port : "0", SOCK_DGRAM, 1, 0); +	if(data->fd < 0){ +		return 1; +	} + +	//bind control port +	if(data->mode == apple){ +		if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ +			fprintf(stderr, "Failed to fetch data port information: %s\n", strerror(errno)); +			return 1; +		} + +		snprintf(control_port, sizeof(control_port), "%d", be16toh(((struct sockaddr_in*)&sock_addr)->sin_port) - 1); +		data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0); +		if(data->control_fd < 0){ +			fprintf(stderr, "Failed to bind control port %s\n", control_port); +			return 1; +		} +	} + +	return 0; +} + +static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len){ +	size_t u; + +	for(u = 0; u < data->peers; u++){ +		//check whether the peer is already in the list +		if(sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ +			return 0; +		} +	} + +	data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); +	if(!data->peer){ +		fprintf(stderr, "Failed to allocate memory\n"); +		data->peers = 0; +		return 1; +	} + +	data->peer[data->peers].dest = sock_addr; +	data->peer[data->peers].dest_len = sock_len; + +	data->peers++; +	return 0; +} + +static int rtpmidi_push_invite(instance* inst, char* peer){ +	size_t u, p; + +	//check whether the instance is already in the announce list +	for(u = 0; u < cfg.announces; u++){ +		if(cfg.announce[u].inst == inst){ +			break; +		} +	} + +	//add to the announce list +	if(u == cfg.announces){ +		cfg.announce = realloc(cfg.announce, (cfg.announces + 1) * sizeof(rtpmidi_announce)); +		if(!cfg.announce){ +			fprintf(stderr, "Failed to allocate memory\n"); +			cfg.announces = 0; +			return 1; +		} + +		cfg.announce[u].inst = inst; +		cfg.announce[u].invites = 0; +		cfg.announce[u].invite = NULL; + +		cfg.announces++; +	} + +	//check whether the peer is already in the invite list +	for(p = 0; p < cfg.announce[u].invites; p++){ +		if(!strcmp(cfg.announce[u].invite[p], peer)){ +			return 0; +		} +	} + +	//extend the invite list +	cfg.announce[u].invite = realloc(cfg.announce[u].invite, (cfg.announce[u].invites + 1) * sizeof(char*)); +	if(!cfg.announce[u].invite){ +		fprintf(stderr, "Failed to allocate memory\n"); +		cfg.announce[u].invites = 0; +		return 1; +	} + +	//append the new invitee +	cfg.announce[u].invite[p] = strdup(peer); +	if(!cfg.announce[u].invite[p]){ +		fprintf(stderr, "Failed to allocate memory\n"); +		return 1; +	} + +	cfg.announce[u].invites++; +	return 0; +} + +static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ +	rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; +	char* host = NULL, *port = NULL; +	struct sockaddr_storage sock_addr; +	socklen_t sock_len = sizeof(sock_addr); + +	if(!strcmp(option, "mode")){ +		if(!strcmp(value, "direct")){ +			data->mode = direct; +			return 0; +		} +		else if(!strcmp(value, "apple")){ +			data->mode = apple; +			return 0; +		} +		fprintf(stderr, "Unknown rtpmidi instance mode %s for instance %s\n", value, inst->name); +		return 1; +	} +	else if(!strcmp(option, "ssrc")){ +		data->ssrc = strtoul(value, NULL, 0); +		if(!data->ssrc){ +			fprintf(stderr, "Random SSRC will be generated for rtpmidi instance %s\n", inst->name); +		} +		return 0; +	} +	else if(!strcmp(option, "bind")){ +		if(data->mode == unconfigured){ +			fprintf(stderr, "Please specify mode for instance %s before setting bind host\n", inst->name); +			return 1; +		} + +		mmbackend_parse_hostspec(value, &host, &port); + +		if(!host){ +			fprintf(stderr, "Could not parse bind host specification %s for instance %s\n", value, inst->name); +			return 1; +		} + +		return rtpmidi_bind_instance(data, host, port); +	} +	else if(!strcmp(option, "learn")){ +		if(data->mode != direct){ +			fprintf(stderr, "The rtpmidi 'learn' option is only valid for direct mode instances\n"); +			return 1; +		} +		data->learn_peers = 0; +		if(!strcmp(value, "true")){ +			data->learn_peers = 1; +		} +		return 0; +	} +	else if(!strcmp(option, "peer")){ +		mmbackend_parse_hostspec(value, &host, &port); +		if(!host || !port){ +			fprintf(stderr, "Invalid peer %s configured on rtpmidi instance %s\n", value, inst->name); +			return 1; +		} + +		if(mmbackend_parse_sockaddr(host, port, &sock_addr, &sock_len)){ +			fprintf(stderr, "Failed to resolve peer %s configured on rtpmidi instance %s\n", value, inst->name); +			return 1; +		} + +		return rtpmidi_push_peer(data, sock_addr, sock_len); +	} +	else if(!strcmp(option, "session")){ +		if(data->mode != apple){ +			fprintf(stderr, "The rtpmidi 'session' option is only valid for apple mode instances\n"); +			return 1; +		} +		free(data->session_name); +		data->session_name = strdup(value); +		if(!data->session_name){ +			fprintf(stderr, "Failed to allocate memory\n"); +			return 1; +		} +		return 0; +	} +	else if(!strcmp(option, "invite")){ +		if(data->mode != apple){ +			fprintf(stderr, "The rtpmidi 'invite' option is only valid for apple mode instances\n"); +			return 1; +		} + +		return rtpmidi_push_invite(inst, value); +	} +	else if(!strcmp(option, "join")){ +		if(data->mode != apple){ +			fprintf(stderr, "The rtpmidi 'join' option is only valid for apple mode instances\n"); +			return 1; +		} +		free(data->accept); +		data->accept = strdup(value); +		if(!data->accept){ +			fprintf(stderr, "Failed to allocate memory\n"); +			return 1; +		} +		return 0; +	} + +	fprintf(stderr, "Unknown rtpmidi instance option %s\n", option); +	return 1; +} + +static instance* rtpmidi_instance(){ +	rtpmidi_instance_data* data = NULL; +	instance* inst = mm_instance(); + +	if(!inst){ +		return NULL; +	} + +	data = calloc(1, sizeof(rtpmidi_instance_data)); +	if(!data){ +		fprintf(stderr, "Failed to allocate memory\n"); +		return NULL; +	} +	data->fd = -1; +	data->control_fd = -1; + +	inst->impl = data; +	return inst; +} + +static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ +	char* next_token = spec; +	rtpmidi_channel_ident ident = { +		.label = 0 +	}; + +	if(!strncmp(spec, "ch", 2)){ +		next_token += 2; +		if(!strncmp(spec, "channel", 7)){ +			next_token = spec + 7; +		} +	} +	else{ +		fprintf(stderr, "Invalid rtpmidi channel specification %s\n", spec); +		return NULL; +	} + +	ident.fields.channel = strtoul(next_token, &next_token, 10); +	if(ident.fields.channel > 15){ +		fprintf(stderr, "rtpmidi channel out of range in channel spec %s\n", spec); +		return NULL; +	} + +	if(*next_token != '.'){ +		fprintf(stderr, "rtpmidi channel specification %s does not conform to channel<X>.<control><Y>\n", spec); +		return NULL; +	} + +	next_token++; + +	if(!strncmp(next_token, "cc", 2)){ +		ident.fields.type = cc; +		next_token += 2; +	} +	else if(!strncmp(next_token, "note", 4)){ +		ident.fields.type = note; +		next_token += 4; +	} +	else if(!strncmp(next_token, "pressure", 8)){ +		ident.fields.type = pressure; +		next_token += 8; +	} +	else if(!strncmp(next_token, "pitch", 5)){ +		ident.fields.type = pitchbend; +	} +	else if(!strncmp(next_token, "aftertouch", 10)){ +		ident.fields.type = aftertouch; +	} +	else{ +		fprintf(stderr, "Unknown rtpmidi channel control type in spec %s\n", spec); +		return NULL; +	} + +	ident.fields.control = strtoul(next_token, NULL, 10); + +	if(ident.label){ +		return mm_channel(inst, ident.label, 1); +	} +	return NULL; +} + +static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ +	rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; +	uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; +	rtpmidi_header* rtp_header = (rtpmidi_header*) frame; +	rtpmidi_command_header* command_header = (rtpmidi_command_header*) (frame + sizeof(rtpmidi_header)); +	size_t offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0; +	uint8_t* payload = frame + offset; +	rtpmidi_channel_ident ident; + +	rtp_header->vpxcc = RTPMIDI_HEADER_MAGIC; +	//some receivers seem to have problems reading rfcs and interpreting the marker bit correctly +	rtp_header->mpt = (data->mode == apple ? 0 : 0x80) | RTPMIDI_HEADER_TYPE; +	rtp_header->sequence = htobe16(data->sequence++); +	rtp_header->timestamp = mm_timestamp() * 10; //just assume 100msec resolution because rfc4695 handwaves it +	rtp_header->ssrc = htobe32(data->ssrc); + +	//midi command section header +	//TODO enable the journal bit here +	command_header->flags = 0xA0; //extended length header, first entry in list has dtime + +	//midi list +	for(u = 0; u < num; u++){ +		ident.label = c[u]->ident; + +		//encode timestamp +		payload[0] = 0; + +		//encode midi command +		payload[1] = ident.fields.type | ident.fields.channel; +		payload[2] = ident.fields.control; +		payload[3] = v[u].normalised * 127.0; + +		if(ident.fields.type == pitchbend){ +			payload[2] = ((int)(v[u].normalised * 16384.0)) & 0x7F; +			payload[3] = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; +		} +		//channel-wide aftertouch is only 2 bytes +		else if(ident.fields.type == aftertouch){ +			payload[2] = payload[3]; +			payload -= 1; +			offset -= 1; +		} + +		payload += 4; +		offset += 4; +	} + +	//update command section length +	//FIXME this might overrun, might check the number of events at some point +	command_header->flags |= (((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0x0F00) >> 8); +	command_header->length = ((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0xFF); + +	//TODO journal section + +	for(u = 0; u < data->peers; u++){ +		sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); +	} + +	return 0; +} + +static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){ +	rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; +	apple_command* command = (apple_command*) frame; +	size_t u; + +	//find peer if already in list +	for(u = 0; u < data->peers; u++){ +		if(data->peer[u].dest_len == peer_len +				&& !memcmp(&data->peer[u].dest, peer, peer_len)){ +			break; +		} +	} + +	if(!strncmp((char*) command->command, APPLEMIDI_INVITE, 2)){ +		//TODO check whether the session is in the accept list +	} +	else if(!strncmp((char*) command->command, APPLEMIDI_ACCEPT, 2)){ +		//TODO mark peer as in-session, start timesync +	} +	else if(!strncmp((char*) command->command, APPLEMIDI_REJECT, 2)){ +		//TODO mark peer as rejected (or retry invitation) +	} +	else if(!strncmp((char*) command->command, APPLEMIDI_LEAVE, 2)){ +		//TODO mark peer as disconnected, retry invitation +	} +	else if(!strncmp((char*) command->command, APPLEMIDI_SYNC, 2)){ +		//TODO respond with sync answer +	} +	else if(!strncmp((char*) command->command, APPLEMIDI_FEEDBACK, 2)){ +		//ignore +	} +	else{ +		fprintf(stderr, "Unknown AppleMIDI session command %02X %02X\n", command->command[0], command->command[1]); +	} + +	return 0; +} + +static int rtpmidi_handle_data(instance* inst){ +	size_t u; +	rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; +	uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; +	struct sockaddr_storage sock_addr; +	socklen_t sock_len = sizeof(sock_addr); +	rtpmidi_header* rtp_header = (rtpmidi_header*) frame; +	ssize_t bytes_recv = recvfrom(data->fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); + +	//TODO receive until EAGAIN +	if(bytes_recv < 0){ +		fprintf(stderr, "rtpmidi failed to receive for instance %s\n", inst->name); +		return 1; +	} + +	if(bytes_recv < sizeof(rtpmidi_header)){ +		fprintf(stderr, "Skipping short packet on rtpmidi instance %s\n", inst->name); +		return 0; +	} + +	//FIXME might want to filter data input from sources that are not registered peers +	if(data->mode == apple && rtp_header->vpxcc == 0xFF && rtp_header->mpt == 0xFF){ +		return rtpmidi_handle_applemidi(inst, data->fd, frame, bytes_recv, &sock_addr, sock_len); +	} +	else if(rtp_header->vpxcc != RTPMIDI_HEADER_MAGIC || RTPMIDI_GET_TYPE(rtp_header->mpt) != RTPMIDI_HEADER_TYPE){ +		fprintf(stderr, "rtpmidi instance %s received frame with invalid header magic\n", inst->name); +		return 0; +	} + +	//TODO parse data + +	//try to learn peers +	if(data->learn_peers){ +		for(u = 0; u < data->peers; u++){ +			if(data->peer[u].dest_len == sock_len +					&& !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ +				break; +			} +		} + +		if(u == data->peers){ +			fprintf(stderr, "rtpmidi instance %s learned new peer\n", inst->name); +			return rtpmidi_push_peer(data, sock_addr, sock_len); +		} +	} +	return 0; +} + +static int rtpmidi_handle_control(instance* inst){ +	rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; +	uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; +	struct sockaddr_storage sock_addr; +	socklen_t sock_len = sizeof(sock_addr); +	ssize_t bytes_recv = recvfrom(data->control_fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); + +	if(bytes_recv < 0){ +		fprintf(stderr, "rtpmidi failed to receive for instance %s\n", inst->name); +		return 1; +	} + +	//the shortest applemidi packet is still larger than the rtpmidi header, so use that as bar +	if(bytes_recv < sizeof(rtpmidi_header)){ +		fprintf(stderr, "Skipping short packet on rtpmidi instance %s\n", inst->name); +		return 0; +	} + +	if(data->mode == apple && frame[0] == 0xFF && frame[1] == 0xFF){ +		return rtpmidi_handle_applemidi(inst, data->control_fd, frame, bytes_recv, &sock_addr, sock_len); +	} + +	fprintf(stderr, "Unknown session protocol frame received on rtpmidi instance %s\n", inst->name); +	return 0; +} + +static int rtpmidi_handle(size_t num, managed_fd* fds){ +	size_t u; +	int rv = 0; +	instance* inst = NULL; +	rtpmidi_instance_data* data = NULL; + +	//TODO handle mDNS discovery frames + +	if(!num){ +		return 0; +	} + +	for(u = 0; u < num; u++){ +		if(!fds[u].impl){ +			//TODO handle mDNS discovery input +		} +		else{ +			//handle rtp/control input +			inst = (instance*) fds[u].impl; +			data = (rtpmidi_instance_data*) inst->impl; +			if(fds[u].fd == data->fd){ +				rv |= rtpmidi_handle_data(inst); +			} +			else if(fds[u].fd == data->control_fd){ +				rv |=  rtpmidi_handle_control(inst); +			} +			else{ +				fprintf(stderr, "rtpmidi signaled descriptor not recognized\n"); +			} +		} +	} + +	return rv; +} + +static int rtpmidi_start(size_t n, instance** inst){ +	size_t u, fds = 0; +	rtpmidi_instance_data* data = NULL; + +	//if mdns name defined and no socket, bind default values +	if(cfg.mdns_name && cfg.mdns_fd < 0){ +		cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); +		if(cfg.mdns_fd < 0){ +			return 1; +		} +	} + +	//register mdns fd to core +	if(cfg.mdns_fd >= 0){ +		if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ +			fprintf(stderr, "rtpmidi failed to register mDNS socket with core\n"); +			return 1; +		} +		fds++; +	} +	else{ +		fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session discovery disabled\n"); +	} + +	for(u = 0; u < n; u++){ +		data = (rtpmidi_instance_data*) inst[u]->impl; +		//check whether instances are explicitly configured to a mode +		if(data->mode == unconfigured){ +			fprintf(stderr, "rtpmidi instance %s is missing a mode configuration\n", inst[u]->name); +			return 1; +		} + +		//generate random ssrc's +		if(!data->ssrc){ +			data->ssrc = rand() << 16 | rand(); +		} + +		//if not bound, bind to default +		if(data->fd < 0 && rtpmidi_bind_instance(data, RTPMIDI_DEFAULT_HOST, NULL)){ +			fprintf(stderr, "Failed to bind default sockets for rtpmidi instance %s\n", inst[u]->name); +			return 1; +		} + +		//register fds to core +		if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){ +			fprintf(stderr, "rtpmidi failed to register instance socket with core\n"); +			return 1; +		} +		fds += (data->control_fd >= 0) ? 2 : 1; +	} + +	fprintf(stderr, "rtpmidi backend registered %" PRIsize_t " descriptors to core\n", fds); +	return 0; +} + +static int rtpmidi_shutdown(size_t n, instance** inst){ +	rtpmidi_instance_data* data = NULL; +	size_t u; + +	for(u = 0; u < n; u++){ +		data = (rtpmidi_instance_data*) inst[u]->impl; +		if(data->fd >= 0){ +			close(data->fd); +		} + +		if(data->control_fd >= 0){ +			close(data->control_fd); +		} + +		free(data->session_name); +		data->session_name = NULL; + +		free(data->accept); +		data->accept = NULL; + +		free(data->peer); +		data->peer = NULL; +		data->peers = 0; + +		free(inst[u]->impl); +		inst[u]->impl = NULL; +	} + +	//TODO free announces +	free(cfg.mdns_name); +	if(cfg.mdns_fd >= 0){ +		close(cfg.mdns_fd); +	} + +	return 0; +} diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h new file mode 100644 index 0000000..2652db7 --- /dev/null +++ b/backends/rtpmidi.h @@ -0,0 +1,124 @@ +#ifndef _WIN32 +#include <sys/socket.h> +#endif +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static int rtpmidi_configure(char* option, char* value); +static int rtpmidi_configure_instance(instance* instance, char* option, char* value); +static instance* rtpmidi_instance(); +static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); +static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); +static int rtpmidi_handle(size_t num, managed_fd* fds); +static int rtpmidi_start(size_t n, instance** inst); +static int rtpmidi_shutdown(size_t n, instance** inst); + +#define RTPMIDI_PACKET_BUFFER 8192 +#define RTPMIDI_DEFAULT_HOST "::" +#define RTPMIDI_MDNS_PORT "5353" +#define RTPMIDI_HEADER_MAGIC 0x80 +#define RTPMIDI_HEADER_TYPE 0x61 +#define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) + +#define APPLEMIDI_INVITE "IN" +#define APPLEMIDI_ACCEPT "OK" +#define APPLEMIDI_REJECT "NO" +#define APPLEMIDI_LEAVE "BY" +#define APPLEMIDI_SYNC "CK" +#define APPLEMIDI_FEEDBACK "RS" + +enum /*_rtpmidi_channel_type*/ { +	none = 0, +	note = 0x90, +	cc = 0xB0, +	pressure = 0xA0, +	aftertouch = 0xD0, +	pitchbend = 0xE0 +}; + +typedef enum /*_rtpmidi_instance_mode*/ { +	unconfigured = 0, +	direct, +	apple +} rtpmidi_instance_mode; + +typedef union { +	struct { +		uint8_t pad[5]; +		uint8_t type; +		uint8_t channel; +		uint8_t control; +	} fields; +	uint64_t label; +} rtpmidi_channel_ident; + +typedef struct /*_rtpmidi_peer*/ { +	struct sockaddr_storage dest; +	socklen_t dest_len; +	uint32_t ssrc; +} rtpmidi_peer; + +typedef struct /*_rtmidi_instance_data*/ { +	rtpmidi_instance_mode mode; + +	int fd; +	int control_fd; + +	size_t peers; +	rtpmidi_peer* peer; +	uint32_t ssrc; +	uint16_t sequence; + +	//apple-midi config +	char* session_name; +	char* accept; + +	//direct mode config +	uint8_t learn_peers; +} rtpmidi_instance_data; + +typedef struct /*rtpmidi_announced_instance*/ { +	instance* inst; +	size_t invites; +	char** invite; +} rtpmidi_announce; + +#pragma pack(push, 1) +typedef struct /*_apple_session_command*/ { +	uint16_t res1; +	uint8_t command[2]; +	uint32_t version; +	uint32_t token; +	uint32_t ssrc; +	//char* name +} apple_command; + +typedef struct /*_apple_session_sync*/ { +	uint16_t res1; +	uint8_t command[2]; +	uint32_t ssrc; +	uint8_t count; +	uint8_t res2[3]; +	uint64_t timestamp[3]; +} apple_sync; + +typedef struct /*_apple_session_feedback*/ { +	uint16_t res1; +	uint8_t command[2]; +	uint32_t ssrc; +	uint32_t sequence; +} apple_feedback; + +typedef struct /*_rtp_midi_header*/ { +	uint8_t vpxcc; +	uint8_t mpt; +	uint16_t sequence; +	uint32_t timestamp; +	uint32_t ssrc; +} rtpmidi_header; + +typedef struct /*_rtp_midi_command*/ { +	uint8_t flags; +	uint8_t length; +} rtpmidi_command_header; +#pragma pack(pop) diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md new file mode 100644 index 0000000..d42df6f --- /dev/null +++ b/backends/rtpmidi.md @@ -0,0 +1,87 @@ +### The `rtpmidi` backend + +This backend provides read-write access to RTP MIDI streams, which transfer MIDI data +over the network. + +As the specification for RTP MIDI does not normatively indicate any method +for session management, most vendors define their own standards for this. +The MIDIMonster supports the following session management methods, which are +selectable per-instance, with some methods requiring additional global configuration: + +* Direct connection: The instance will send and receive data from peers configured in the +	instance configuration +* Direct connection with peer learning: The instance will send and receive data from peers +	configured in the instance configuration as well as previously unknown peers that +	voluntarily send data to the instance. +* AppleMIDI session management: + +Note that instances that receive data from multiple peers will combine all inputs into one +stream, which may lead to inconsistencies during playback. + +#### Global configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `detect`      | `on`                  | `off`                 | Output channel specifications for any events coming in on configured instances to help with configuration | +| `mdns-bind`	| `10.1.2.1 5353`	| `:: 5353`		| Bind host for the mDNS discovery server | +| `mdns-name`	| `computer1`		| none			| mDNS hostname to announce, also used as AppleMIDI peer name | + +#### Instance configuration + +Common instance configuration parameters + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `ssrc`	| `0xDEADBEEF`		| Randomly generated	| 32-bit synchronization source identifier | +| `mode`	| `direct`		| none			| Instance session management mode (`direct` or `apple`) | +| `peer`	| `10.1.2.3 9001`	| none			| MIDI session peer, may be specified multiple times. Bypasses session discovery protocols | + +`direct` mode instance configuration parameters + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `bind`	| `10.1.2.1 9001`	| `:: <random>`		| Local network address to bind to |  +| `learn`	| `true`		| `false`		| Accept new peers for data exchange at runtime | + +`apple` mode instance configuration parameters + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `bind`	| `10.1.2.1 9001`	| `:: <random>`		| Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | +| `session`	| `Just Jamming`	| `MIDIMonster`		| Session name to announce via mDNS | +| `invite`	| `pad`			| none			| Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times | +| `join`	| `Just Jamming`	| none			| Session for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | +| `peer`	| `10.1.2.3 9001`	| none			| Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | + +Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter +(and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. + +#### Channel specification + +The `rtpmidi` backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are + +* `cc` - Control Changes +* `note` - Note On/Off messages +* `pressure` - Note pressure/aftertouch messages +* `aftertouch` - Channel-wide aftertouch messages +* `pitch` - Channel pitchbend messages + +A MIDIMonster channel is specified using the syntax `channel<channel>.<type><index>`. The shorthand `ch` may be +used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). + +The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`. + +MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which +additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called +'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. + +Example mappings: + +``` +rmidi1.ch0.note9 > rmidi2.channel1.cc4 +rmidi1.channel15.pressure1 > rmidi1.channel0.note0 +rmidi1.ch1.aftertouch > rmidi2.ch2.cc0 +rmidi1.ch0.pitch > rmidi2.ch1.pitch +``` + +#### Known bugs / problems  | 
