diff options
Diffstat (limited to 'backends')
36 files changed, 6010 insertions, 701 deletions
| diff --git a/backends/Makefile b/backends/Makefile index 771e97e..df01ec8 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,10 +1,14 @@ -.PHONY: all clean +.PHONY: all clean full  LINUX_BACKENDS = midi.so evdev.so -BACKENDS = artnet.so osc.so loopback.so sacn.so rtpmidi.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  SYSTEM := $(shell uname -s) -CFLAGS += -fPIC -I../ +CFLAGS += -g -fPIC -I../ -Wall -Wpedantic +CPPFLAGS += -g -fPIC -I../  LDFLAGS += -shared  # Build Linux backends if possible @@ -16,14 +20,63 @@ ifeq ($(SYSTEM),Darwin)  LDFLAGS += -undefined dynamic_lookup  endif +artnet.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +artnet.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +artnet.dll: LDLIBS += -lws2_32 + +osc.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +osc.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +osc.dll: LDLIBS += -lws2_32 + +sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +sacn.dll: LDLIBS += -lws2_32 + +maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +maweb.so: LDLIBS = -lssl +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 + +jack.so: LDLIBS = -ljack -lpthread  midi.so: LDLIBS = -lasound -evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) -evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) +evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev || echo "-DBUILD_ERROR=\"Missing pkg-config data for libevdev\"") +evdev.so: LDLIBS = $(shell pkg-config --libs libevdev || echo "-DBUILD_ERROR=\"Missing pkg-config data for libevdev\"") +ola.so: LDLIBS = -lola +ola.so: CPPFLAGS += -Wno-write-strings +# The pkg-config name for liblua5.3 is subject to discussion. I prefer 'lua5.3' (which works on Debian and OSX), +# but Arch requires 'lua53' which works on Debian, too, but breaks on OSX. +lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") +lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") + +%.so :: %.c %.h $(BACKEND_LIB) +	$(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) + +%.dll :: %.c %.h $(BACKEND_LIB) +	$(CC) $(CFLAGS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) $(LDLIBS) + +%.so :: %.cpp %.h +	$(CXX) $(CPPFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) + +all: $(BACKEND_LIB) $(BACKENDS) + +../libmmapi.a: +	$(MAKE) -C ../ midimonster.exe -%.so :: %.c %.h -	$(CC) $(CFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS) +windows: export CC = x86_64-w64-mingw32-gcc +windows: LDLIBS += -lmmapi +windows: LDFLAGS += -L../ +windows: CFLAGS += -Wno-format -Wno-pointer-sign +windows: ../libmmapi.a $(BACKEND_LIB) $(WINDOWS_BACKENDS) -all: $(BACKENDS) +full: $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS)  clean: -	$(RM) $(BACKENDS) +	$(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) $(WINDOWS_BACKENDS) diff --git a/backends/artnet.c b/backends/artnet.c index d9ebfe5..57eb7b1 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -1,13 +1,10 @@  #include <string.h> -#include <sys/types.h> -#include <sys/socket.h> -#include <netdb.h> -#include <unistd.h> -#include <fcntl.h>  #include <ctype.h>  #include <errno.h> +#include "libmmbackend.h"  #include "artnet.h" +  #define MAX_FDS 255  #define BACKEND_NAME "artnet" @@ -16,68 +13,14 @@ static size_t artnet_fds = 0;  static artnet_descriptor* artnet_fd = NULL;  static int artnet_listener(char* host, char* port){ -	int fd = -1, status, yes = 1, flags; -	struct addrinfo hints = { -		.ai_family = AF_UNSPEC, -		.ai_socktype = SOCK_DGRAM, -		.ai_flags = AI_PASSIVE -	}; -	struct addrinfo* info; -	struct addrinfo* addr_it; - +	int fd;  	if(artnet_fds >= MAX_FDS){  		fprintf(stderr, "ArtNet backend descriptor limit reached\n");  		return -1;  	} -	status = getaddrinfo(host, port, &hints, &info); -	if(status){ -		fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); -		return -1; -	} - -	for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ -		fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); -		if(fd < 0){ -			continue; -		} - -		yes = 1; -		if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ -			fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); -		} - -		yes = 1; -		if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ -			fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); -		} - -		yes = 0; -		if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ -			fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); -		} - -		status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); -		if(status < 0){ -			close(fd); -			continue; -		} - -		break; -	} - -	freeaddrinfo(info); - -	if(!addr_it){ -		fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); -		return -1; -	} - -	//set nonblocking -	flags = fcntl(fd, F_GETFL, 0); -	if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ -		fprintf(stderr, "Failed to set ArtNet descriptor nonblocking\n"); -		close(fd); +	fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); +	if(fd < 0){  		return -1;  	} @@ -89,7 +32,7 @@ static int artnet_listener(char* host, char* port){  		return -1;  	} -	fprintf(stderr, "ArtNet backend interface %zu bound to %s port %s\n", artnet_fds, host, port); +	fprintf(stderr, "ArtNet backend interface %" PRIsize_t " bound to %s port %s\n", artnet_fds, host, port);  	artnet_fd[artnet_fds].fd = fd;  	artnet_fd[artnet_fds].output_instances = 0;  	artnet_fd[artnet_fds].output_instance = NULL; @@ -98,51 +41,7 @@ static int artnet_listener(char* host, char* port){  	return 0;  } -static int artnet_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ -	struct addrinfo* head; -	struct addrinfo hints = { -		.ai_family = AF_UNSPEC, -		.ai_socktype = SOCK_DGRAM -	}; - -	int error = getaddrinfo(host, port, &hints, &head); -	if(error || !head){ -		fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); -		return 1; -	} - -	memcpy(addr, head->ai_addr, head->ai_addrlen); -	*len = head->ai_addrlen; - -	freeaddrinfo(head); -	return 0; -} - -static int artnet_separate_hostspec(char* in, char** host, char** port){ -	size_t u; - -	if(!in || !host || !port){ -		return 1; -	} - -	for(u = 0; in[u] && !isspace(in[u]); u++){ -	} - -	//guess -	*host = in; - -	if(in[u]){ -		in[u] = 0; -		*port = in + u + 1; -	} -	else{ -		//no port given -		*port = ARTNET_PORT; -	} -	return 0; -} - -int init(){ +MM_PLUGIN_API int init(){  	backend artnet = {  		.name = BACKEND_NAME,  		.conf = artnet_configure, @@ -155,6 +54,11 @@ int init(){  		.shutdown = artnet_shutdown  	}; +	if(sizeof(artnet_instance_id) != sizeof(uint64_t)){ +		fprintf(stderr, "ArtNet instance identification union out of bounds\n"); +		return 1; +	} +  	//register backend  	if(mm_backend_register(artnet)){  		fprintf(stderr, "Failed to register ArtNet backend\n"); @@ -171,8 +75,14 @@ static int artnet_configure(char* option, char* value){  		return 0;  	}  	else if(!strcmp(option, "bind")){ -		if(artnet_separate_hostspec(value, &host, &port)){ -			fprintf(stderr, "Not a valid ArtNet bind address: %s\n", value); +		mmbackend_parse_hostspec(value, &host, &port); + +		if(!port){ +			port = ARTNET_PORT; +		} + +		if(!host){ +			fprintf(stderr, "Not valid ArtNet bind address given\n");  			return 1;  		} @@ -228,19 +138,25 @@ static int artnet_configure_instance(instance* inst, char* option, char* value){  		return 0;  	}  	else if(!strcmp(option, "dest") || !strcmp(option, "destination")){ -		if(artnet_separate_hostspec(value, &host, &port)){ +		mmbackend_parse_hostspec(value, &host, &port); + +		if(!port){ +			port = ARTNET_PORT; +		} + +		if(!host){  			fprintf(stderr, "Not a valid ArtNet destination for instance %s\n", inst->name);  			return 1;  		} -		return artnet_parse_addr(host, port, &data->dest_addr, &data->dest_len); +		return mmbackend_parse_sockaddr(host, port, &data->dest_addr, &data->dest_len);  	}  	fprintf(stderr, "Unknown ArtNet option %s for instance %s\n", option, inst->name);  	return 1;  } -static channel* artnet_channel(instance* inst, char* spec){ +static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){  	artnet_instance_data* data = (artnet_instance_data*) inst->impl;  	char* spec_next = spec;  	unsigned chan_a = strtoul(spec, &spec_next, 10); @@ -301,7 +217,7 @@ static int artnet_transmit(instance* inst){  	};  	memcpy(frame.data, data->data.out, 512); -	if(sendto(artnet_fd[data->fd_index].fd, &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ +	if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){  		fprintf(stderr, "Failed to output ArtNet frame for instance %s: %s\n", inst->name, strerror(errno));  	} @@ -319,7 +235,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v)  	artnet_instance_data* data = (artnet_instance_data*) inst->impl;  	if(!data->dest_len){ -		fprintf(stderr, "ArtNet instance %s not enabled for output (%zu channel events)\n", inst->name, num); +		fprintf(stderr, "ArtNet instance %s not enabled for output (%" PRIsize_t " channel events)\n", inst->name, num);  		return 0;  	} @@ -384,7 +300,7 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){  			}  			if(!chan){ -				fprintf(stderr, "Active channel %zu on %s not known to core\n", p, inst->name); +				fprintf(stderr, "Active channel %" PRIsize_t " on %s not known to core\n", p, inst->name);  				return 1;  			} @@ -456,7 +372,11 @@ static int artnet_handle(size_t num, managed_fd* fds){  			}  		} while(bytes_read > 0); +		#ifdef _WIN32 +		if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){ +		#else  		if(bytes_read < 0 && errno != EAGAIN){ +		#endif  			fprintf(stderr, "ArtNet failed to receive data: %s\n", strerror(errno));  		} @@ -527,7 +447,7 @@ static int artnet_start(){  		}  	} -	fprintf(stderr, "ArtNet backend registering %zu descriptors to core\n", artnet_fds); +	fprintf(stderr, "ArtNet backend registering %" PRIsize_t " descriptors to core\n", artnet_fds);  	for(u = 0; u < artnet_fds; u++){  		if(mm_manage_fd(artnet_fd[u].fd, BACKEND_NAME, 1, (void*) u)){  			goto bail; diff --git a/backends/artnet.h b/backends/artnet.h index 90aedd5..f6a6709 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -1,11 +1,13 @@ +#ifndef _WIN32  #include <sys/socket.h> +#endif  #include "midimonster.h" -int init(); +MM_PLUGIN_API int init();  static int artnet_configure(char* option, char* value);  static int artnet_configure_instance(instance* instance, char* option, char* value);  static instance* artnet_instance(); -static channel* artnet_channel(instance* instance, char* spec); +static channel* artnet_channel(instance* instance, char* spec, uint8_t flags);  static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v);  static int artnet_handle(size_t num, managed_fd* fds);  static int artnet_start(); diff --git a/backends/artnet.md b/backends/artnet.md new file mode 100644 index 0000000..90a7697 --- /dev/null +++ b/backends/artnet.md @@ -0,0 +1,41 @@ +### The `artnet` backend + +The ArtNet backend provides read-write access to the UDP-based ArtNet protocol for lighting +fixture control. + +#### Global configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `bind`	| `127.0.0.1 6454`	| none		| Binds a network address to listen for data. This option may be set multiple times, with each interface being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one interface is required for transmission. | +| `net`		| `0`			| `0`			| The default net to use | + +#### Instance configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `net`		| `0`			| `0`			| ArtNet `net` to use	| +| `universe`	| `0`			| `0`			| Universe identifier	| +| `destination`	| `10.2.2.2`		| none			| Destination address for sent ArtNet frames. Setting this enables the universe for output | +| `interface`	| `1`			| `0`			| The bound address to use for data input/output | + +#### Channel specification + +A channel is specified by it's universe index. Channel indices start at 1 and end at 512. + +Example mapping: +``` +net1.231 < net2.123 +``` + +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax +``` +net1.1+2 > net2.5+123 +``` + +A normal channel that is part of a wide channel can not be mapped individually. + +#### Known bugs / problems + +The minimum inter-frame-time is disregarded, as the packet rate is determined by the rate of incoming +channel events.
\ No newline at end of file diff --git a/backends/evdev.c b/backends/evdev.c index 979698f..0da5ae6 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -18,16 +18,13 @@  #define BACKEND_NAME "evdev" -typedef union { -	struct { -		uint32_t pad; -		uint16_t type; -		uint16_t code; -	} fields; -	uint64_t label; -} evdev_channel_ident; - -int init(){ +static struct { +	uint8_t detect; +} evdev_config = { +	.detect = 0 +}; + +MM_PLUGIN_API int init(){  	backend evdev = {  		.name = BACKEND_NAME,  		.conf = evdev_configure, @@ -40,6 +37,11 @@ int init(){  		.shutdown = evdev_shutdown  	}; +	if(sizeof(evdev_channel_ident) != sizeof(uint64_t)){ +		fprintf(stderr, "evdev channel identification union out of bounds\n"); +		return 1; +	} +  	if(mm_backend_register(evdev)){  		fprintf(stderr, "Failed to register evdev backend\n");  		return 1; @@ -49,7 +51,15 @@ int init(){  }  static int evdev_configure(char* option, char* value) { -	fprintf(stderr, "The evdev backend does not take any global configuration\n"); +	if(!strcmp(option, "detect")){ +		evdev_config.detect = 1; +		if(!strcmp(value, "off")){ +			evdev_config.detect = 0; +		} +		return 0; +	} + +	fprintf(stderr, "Unknown configuration option %s for evdev backend\n", option);  	return 1;  } @@ -176,23 +186,48 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) {  			return 1;  		}  		free(next_token); +		return 0;  	}  	else if(!strcmp(option, "exclusive")){  		if(data->input_fd >= 0 && libevdev_grab(data->input_ev, LIBEVDEV_GRAB)){  			fprintf(stderr, "Failed to obtain exclusive device access on %s\n", inst->name);  		}  		data->exclusive = 1; +		return 0; +	} +	else if(!strncmp(option, "relaxis.", 8)){ +		data->relative_axis = realloc(data->relative_axis, (data->relative_axes + 1) * sizeof(evdev_relaxis_config)); +		if(!data->relative_axis){ +			fprintf(stderr, "Failed to allocate memory\n"); +			return 1; +		} +		data->relative_axis[data->relative_axes].inverted = 0; +		data->relative_axis[data->relative_axes].code = libevdev_event_code_from_name(EV_REL, option + 8); +		data->relative_axis[data->relative_axes].max = strtoll(value, &next_token, 0); +		if(data->relative_axis[data->relative_axes].max < 0){ +			data->relative_axis[data->relative_axes].max *= -1; +			data->relative_axis[data->relative_axes].inverted = 1; +		} +		data->relative_axis[data->relative_axes].current = strtoul(next_token, NULL, 0); +		if(data->relative_axis[data->relative_axes].code < 0){ +			fprintf(stderr, "Failed to configure relative axis extents for %s.%s\n", inst->name, option + 8); +			return 1; +		} +		data->relative_axes++; +		return 0;  	}  #ifndef EVDEV_NO_UINPUT  	else if(!strcmp(option, "output")){  		data->output_enabled = 1;  		libevdev_set_name(data->output_proto, value); +		return 0;  	}  	else if(!strcmp(option, "id")){  		next_token = value;  		libevdev_set_id_vendor(data->output_proto, strtol(next_token, &next_token, 0));  		libevdev_set_id_product(data->output_proto, strtol(next_token, &next_token, 0));  		libevdev_set_id_version(data->output_proto, strtol(next_token, &next_token, 0)); +		return 0;  	}  	else if(!strncmp(option, "axis.", 5)){  		//value minimum maximum fuzz flat resolution @@ -207,16 +242,14 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) {  			fprintf(stderr, "Failed to enable absolute axis %s for output\n", option + 5);  			return 1;  		} +		return 0;  	}  #endif -	else{ -		fprintf(stderr, "Unknown configuration parameter %s for evdev backend\n", option); -		return 1; -	} -	return 0; +	fprintf(stderr, "Unknown instance configuration parameter %s for evdev instance %s\n", option, inst->name); +	return 1;  } -static channel* evdev_channel(instance* inst, char* spec){ +static channel* evdev_channel(instance* inst, char* spec, uint8_t flags){  #ifndef EVDEV_NO_UINPUT  	evdev_instance_data* data = (evdev_instance_data*) inst->impl;  #endif @@ -273,21 +306,35 @@ static int evdev_push_event(instance* inst, evdev_instance_data* data, struct in  		.fields.code = event.code  	};  	channel* chan = mm_channel(inst, ident.label, 0); +	size_t axis;  	if(chan){  		val.raw.u64 = event.value;  		switch(event.type){  			case EV_REL: -				val.normalised = 0.5 + ((event.value < 0) ? 0.5 : -0.5); +				for(axis = 0; axis < data->relative_axes; axis++){ +					if(data->relative_axis[axis].code == event.code){ +						if(data->relative_axis[axis].inverted){ +							event.value *= -1; +						} +						data->relative_axis[axis].current = clamp(data->relative_axis[axis].current + event.value, data->relative_axis[axis].max, 0); +						val.normalised = (double) data->relative_axis[axis].current / (double) data->relative_axis[axis].max; +						break; +					} +				} +				if(axis == data->relative_axes){ +					val.normalised = 0.5 + ((event.value < 0) ? 0.5 : -0.5); +					break; +				}  				break;  			case EV_ABS:  				range = libevdev_get_abs_maximum(data->input_ev, event.code) - libevdev_get_abs_minimum(data->input_ev, event.code); -				val.normalised = (event.value - libevdev_get_abs_minimum(data->input_ev, event.code)) / (double) range; +				val.normalised = clamp((event.value - libevdev_get_abs_minimum(data->input_ev, event.code)) / (double) range, 1.0, 0.0);  				break;  			case EV_KEY:  			case EV_SW:  			default: -				val.normalised = 1.0 * event.value; +				val.normalised = clamp(1.0 * event.value, 1.0, 0.0);  				break;  		} @@ -297,6 +344,10 @@ static int evdev_push_event(instance* inst, evdev_instance_data* data, struct in  		}  	} +	if(evdev_config.detect){ +		fprintf(stderr, "Incoming evdev data for channel %s.%s.%s\n", inst->name, libevdev_event_type_get_name(event.type), libevdev_event_code_get_name(event.type, event.code)); +	} +  	return 0;  } @@ -327,6 +378,11 @@ static int evdev_handle(size_t num, managed_fd* fds){  				read_flags = LIBEVDEV_READ_FLAG_SYNC;  			} +			//exclude synchronization events +			if(ev.type == EV_SYN){ +				continue; +			} +  			//handle event  			if(evdev_push_event(inst, data, ev)){  				return 1; @@ -376,6 +432,10 @@ static int evdev_start(){  			fds++;  		} +		if(data->input_fd <= 0 && !data->output_ev){ +			fprintf(stderr, "Instance %s has neither input nor output device set up\n", inst[u]->name); +		} +  	}  	fprintf(stderr, "evdev backend registered %zu descriptors to core\n", fds); @@ -385,7 +445,7 @@ static int evdev_start(){  static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) {  #ifndef EVDEV_NO_UINPUT -	size_t evt = 0; +	size_t evt = 0, axis = 0;  	evdev_instance_data* data = (evdev_instance_data*) inst->impl;  	evdev_channel_ident ident = {  		.label = 0 @@ -407,7 +467,20 @@ static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v)  		switch(ident.fields.type){  			case EV_REL: -				value = (v[evt].normalised < 0.5) ? -1 : ((v[evt].normalised > 0.5) ? 1 : 0); +				for(axis = 0; axis < data->relative_axes; axis++){ +					if(data->relative_axis[axis].code == ident.fields.code){ +						value = (v[evt].normalised * data->relative_axis[axis].max) - data->relative_axis[axis].current; +						data->relative_axis[axis].current = v[evt].normalised * data->relative_axis[axis].max; + +						if(data->relative_axis[axis].inverted){ +							value *= -1; +						} +						break; +					} +				} +				if(axis == data->relative_axes){ +					value = (v[evt].normalised < 0.5) ? -1 : ((v[evt].normalised > 0.5) ? 1 : 0); +				}  				break;  			case EV_ABS:  				range = libevdev_get_abs_maximum(data->output_proto, ident.fields.code) - libevdev_get_abs_minimum(data->output_proto, ident.fields.code); @@ -464,9 +537,12 @@ static int evdev_shutdown(){  		libevdev_free(data->output_proto);  #endif +		data->relative_axes = 0; +		free(data->relative_axis);  		free(data);  	}  	free(instances); +	fprintf(stderr, "evdev backend shut down\n");  	return 0;  } diff --git a/backends/evdev.h b/backends/evdev.h index c6e3a25..6504416 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -8,11 +8,11 @@   * disabled by building with -DEVDEV_NO_UINPUT   */ -int init(); +MM_PLUGIN_API int init();  static int evdev_configure(char* option, char* value);  static int evdev_configure_instance(instance* instance, char* option, char* value);  static instance* evdev_instance(); -static channel* evdev_channel(instance* instance, char* spec); +static channel* evdev_channel(instance* instance, char* spec, uint8_t flags);  static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v);  static int evdev_handle(size_t num, managed_fd* fds);  static int evdev_start(); @@ -24,10 +24,19 @@ static int evdev_shutdown();  	#define UINPUT_MAX_NAME_SIZE 512  #endif +typedef struct /*_evdev_relative_axis_config*/ { +	uint8_t inverted; +	int code; +	int64_t max; +	int64_t current; +} evdev_relaxis_config; +  typedef struct /*_evdev_instance_model*/ {  	int input_fd;  	struct libevdev* input_ev;  	int exclusive; +	size_t relative_axes; +	evdev_relaxis_config* relative_axis;  	int output_enabled;  #ifndef EVDEV_NO_UINPUT @@ -35,3 +44,13 @@ typedef struct /*_evdev_instance_model*/ {  	struct libevdev_uinput* output_ev;  #endif  } evdev_instance_data; + +typedef union { +	struct { +		uint32_t pad; +		uint16_t type; +		uint16_t code; +	} fields; +	uint64_t label; +} evdev_channel_ident; + diff --git a/backends/evdev.md b/backends/evdev.md new file mode 100644 index 0000000..d57201d --- /dev/null +++ b/backends/evdev.md @@ -0,0 +1,86 @@ +### The `evdev` backend + +This backend allows using Linux `evdev` devices such as mouses, keyboards, gamepads and joysticks +as input and output devices. All buttons and axes available to the Linux system are mappable. +Output is provided by the `uinput` kernel module, which allows creation of virtual input devices. +This functionality may require elevated privileges (such as special group membership or root access). + +#### 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. | + +#### Instance configuration + +| Option	| Example value		| Default value | Description						| +|---------------|-----------------------|---------------|-------------------------------------------------------| +| `device`	| `/dev/input/event1`	| none		| `evdev` device to use as input device			| +| `input`	| `Xbox Wireless`	| none		| Presentation name of evdev device to use as input (prefix-matched) | +| `output`	| `My Input Device`	| none		| Output device presentation name. Setting this option enables the instance for output	| +| `exclusive`	| `1`			| `0`		| Prevent other processes from using the device		| +| `id`		| `0x1 0x2 0x3`		| none		| Set output device bus identification (Vendor, Product and Version), optional | +| `axis.AXISNAME`| `34300 0 65536 255 4095` | none	| Specify absolute axis details (see below) for output. This is required for any absolute axis to be output. |  +| `relaxis.AXISNAME`| `65534 32767` | none	| Specify relative axis details (extent and optional initial value) for output and input (see below). | + +The absolute axis details configuration (e.g. `axis.ABS_X`) is required for any absolute axis on output-enabled +instances. The configuration value contains, space-separated, the following values: + +* `value`: The value to assume for the axis until an event is received +* `minimum`: The axis minimum value +* `maximum`: The axis maximum value +* `fuzz`: A value used for filtering the input stream +* `flat`: An offset, below which all deviations will be ignored +* `resolution`: Axis resolution in units per millimeter (or units per radian for rotational axes) + +If an axis is not used for output, this configuration can be omitted. + +For real devices, all of these parameters for every axis can be found by running `evtest` on the device. + +To use the input from relative axes in absolute-value based protocols, the backend needs a reference frame to +convert the relative movements to absolute values. To invert the mapping of the relative axis, specify the `max` value +as a negative number, for example: + +``` +relaxis.REL_X = -1024 512 +``` + +If relative axes are used without specifying their extents, the channel will generate normalized values +of `0`, `0.5` and `1` for any input less than, equal to and greater than `0`, respectively. As for output, only +the values `-1`, `0` and `1` are generated for the same interval. + + +#### Channel specification + +A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes +see the [kernel documentation](https://www.kernel.org/doc/html/v4.12/input/event-codes.html). The most interesting event types are + +* `EV_KEY` for keys and buttons +* `EV_ABS` for absolute axes (such as Joysticks) +* `EV_REL` for relative axes (such as Mouses) + +The `evtest` tool is useful to gather information on devices active on the local system, including names, types, codes +and configuration supported by these devices. + +Example mapping: +``` +ev1.EV_KEY.KEY_A > ev1.EV_ABS.ABS_X +``` + +Note that to map an absolute axis on an output-enabled instance, additional information such as the axis minimum +and maximum are required. These must be specified in the instance configuration. When only mapping the instance +as a channel input, this is not required. + +#### Known bugs / problems + +Creating an `evdev` output device requires elevated privileges, namely, write access to the system's +`/dev/uinput`. Usually, this is granted for users in the `input` group and the `root` user. + +Input devices may synchronize logically connected event types (for example, X and Y axes) via `EV_SYN`-type +events. The MIDIMonster also generates these events after processing channel events, but may not keep the original +event grouping. + +`EV_KEY` key-down events are sent for normalized channel values over `0.9`. + +Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file +but may or may not work with the internal channel mapping and normalization code. diff --git a/backends/jack.c b/backends/jack.c new file mode 100644 index 0000000..e7bed04 --- /dev/null +++ b/backends/jack.c @@ -0,0 +1,748 @@ +#include <string.h> +#include <signal.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "jack.h" +#include <jack/midiport.h> +#include <jack/metadata.h> + +#define BACKEND_NAME "jack" +#define JACKEY_SIGNAL_TYPE "http://jackaudio.org/metadata/signal-type" + +#ifdef __APPLE__ +	#ifndef PTHREAD_MUTEX_ADAPTIVE_NP +		#define PTHREAD_MUTEX_ADAPTIVE_NP PTHREAD_MUTEX_DEFAULT +	#endif +#endif + +//FIXME pitchbend range is somewhat oob + +static struct /*_mmjack_backend_cfg*/ { +	unsigned verbosity; +	volatile sig_atomic_t jack_shutdown; +} config = { +	.verbosity = 1, +	.jack_shutdown = 0 +}; + +MM_PLUGIN_API int init(){ +	backend mmjack = { +		.name = BACKEND_NAME, +		.conf = mmjack_configure, +		.create = mmjack_instance, +		.conf_instance = mmjack_configure_instance, +		.channel = mmjack_channel, +		.handle = mmjack_set, +		.process = mmjack_handle, +		.start = mmjack_start, +		.shutdown = mmjack_shutdown +	}; + +	if(sizeof(mmjack_channel_ident) != sizeof(uint64_t)){ +		fprintf(stderr, "jack channel identification union out of bounds\n"); +		return 1; +	} + +	//register backend +	if(mm_backend_register(mmjack)){ +		fprintf(stderr, "Failed to register jack backend\n"); +		return 1; +	} +	return 0; +} + +static void mmjack_message_print(const char* msg){ +	fprintf(stderr, "JACK message: %s\n", msg); +} + +static void mmjack_message_ignore(const char* msg){ +} + +static int mmjack_midiqueue_append(mmjack_port* port, mmjack_channel_ident ident, uint16_t value){ +	//append events +	if(port->queue_len == port->queue_alloc){ +		//extend the queue +		port->queue = realloc(port->queue, (port->queue_len + JACK_MIDIQUEUE_CHUNK) * sizeof(mmjack_midiqueue)); +		if(!port->queue){ +			fprintf(stderr, "Failed to allocate memory\n"); +			return 1; +		} +		port->queue_alloc += JACK_MIDIQUEUE_CHUNK; +	} + +	port->queue[port->queue_len].ident.label = ident.label; +	port->queue[port->queue_len].raw = value; +	port->queue_len++; +	DBGPF("Appended event to queue for %s, now at %" PRIsize_t " entries\n", port->name, port->queue_len); +	return 0; +} + +static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){ +	void* buffer = jack_port_get_buffer(port->port, nframes); +	jack_nframes_t event_count = jack_midi_get_event_count(buffer); +	jack_midi_event_t event; +	jack_midi_data_t* event_data; +	mmjack_channel_ident ident; +	size_t u; +	uint16_t value; + +	if(port->input){ +		if(event_count){ +			DBGPF("Reading %u MIDI events from jack port %s\n", event_count, port->name); +			for(u = 0; u < event_count; u++){ +				ident.label = 0; +				//read midi data from stream +				jack_midi_event_get(&event, buffer, u); +				//ident.fields.port set on output in mmjack_handle_midi +				ident.fields.sub_channel = event.buffer[0] & 0x0F; +				ident.fields.sub_type = event.buffer[0] & 0xF0; +				ident.fields.sub_control = event.buffer[1]; +				value = event.buffer[2]; +				if(ident.fields.sub_type == 0x80){ +					ident.fields.sub_type = midi_note; +					value = 0; +				} +				else if(ident.fields.sub_type == midi_pitchbend){ +					ident.fields.sub_control = 0; +					value = event.buffer[1] | (event.buffer[2] << 7); +				} +				else if(ident.fields.sub_type == midi_aftertouch){ +					ident.fields.sub_control = 0; +					value = event.buffer[1]; +				} +				//append midi data +				mmjack_midiqueue_append(port, ident, value); +			} +			port->mark = 1; +			*mark = 1; +		} +	} +	else{ +		//clear buffer +		jack_midi_clear_buffer(buffer); + +		for(u = 0; u < port->queue_len; u++){ +			//build midi event +			ident.label = port->queue[u].ident.label; +			event_data = jack_midi_event_reserve(buffer, u, (ident.fields.sub_type == midi_aftertouch) ? 2 : 3); +			if(!event_data){ +				fprintf(stderr, "Failed to reserve MIDI stream data\n"); +				return 1; +			} +			event_data[0] = ident.fields.sub_channel | ident.fields.sub_type; +			if(ident.fields.sub_type == midi_pitchbend){ +				event_data[1] = port->queue[u].raw & 0x7F; +				event_data[2] = (port->queue[u].raw >> 7) & 0x7F; +			} +			else if(ident.fields.sub_type == midi_aftertouch){ +				event_data[1] = port->queue[u].raw & 0x7F; +			} +			else{ +				event_data[1] = ident.fields.sub_control; +				event_data[2] = port->queue[u].raw & 0x7F; +			} +		} + +		if(port->queue_len){ +			DBGPF("Wrote %" PRIsize_t " MIDI events to jack port %s\n", port->queue_len, port->name); +		} +		port->queue_len = 0; +	} +	return 0; +} + +static int mmjack_process_cv(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){ +	jack_default_audio_sample_t* audio_buffer = jack_port_get_buffer(port->port, nframes); +	size_t u; + +	if(port->input){ +		//read updated data into the local buffer +		//FIXME maybe we don't want to always use the first sample... +		if((double) audio_buffer[0] != port->last){ +			port->last = audio_buffer[0]; +			port->mark = 1; +			*mark = 1; +		} +	} +	else{ +		for(u = 0; u < nframes; u++){ +			audio_buffer[u] = port->last; +		} +	} +	return 0; +} + +static int mmjack_process(jack_nframes_t nframes, void* instp){ +	instance* inst = (instance*) instp; +	mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; +	size_t p, mark = 0; +	int rv = 0; + +	//DBGPF("jack callback for %d frames on %s\n", nframes, inst->name); + +	for(p = 0; p < data->ports; p++){ +		pthread_mutex_lock(&data->port[p].lock); +		switch(data->port[p].type){ +			case port_midi: +				//DBGPF("Handling MIDI port %s.%s\n", inst->name, data->port[p].name); +				rv |= mmjack_process_midi(inst, data->port + p, nframes, &mark); +				break; +			case port_cv: +				//DBGPF("Handling CV port %s.%s\n", inst->name, data->port[p].name); +				rv |= mmjack_process_cv(inst, data->port + p, nframes, &mark); +				break; +			default: +				fprintf(stderr, "Unhandled jack port type in processing callback\n"); +				pthread_mutex_unlock(&data->port[p].lock); +				return 1; +		} +		pthread_mutex_unlock(&data->port[p].lock); +	} + +	//notify the main thread +	if(mark){ +		DBGPF("Notifying handler thread for jack instance %s\n", inst->name); +		send(data->fd, "c", 1, 0); +	} +	return rv; +} + +static void mmjack_server_shutdown(void* inst){ +	fprintf(stderr, "jack server shutdown notification\n"); +	config.jack_shutdown = 1; +} + +static int mmjack_configure(char* option, char* value){ +	if(!strcmp(option, "debug")){ +		if(!strcmp(value, "on")){ +			config.verbosity |= 2; +			return 0; +		} +		config.verbosity &= ~2; +		return 0; +	} +	if(!strcmp(option, "errors")){ +		if(!strcmp(value, "on")){ +			config.verbosity |= 1; +			return 0; +		} +		config.verbosity &= ~1; +		return 0; +	} + +	fprintf(stderr, "Unknown jack backend option %s\n", option); +	return 1; +} + +static int mmjack_parse_portconfig(mmjack_port* port, char* spec){ +	char* token = NULL; + +	for(token = strtok(spec, " "); token; token = strtok(NULL, " ")){ +		if(!strcmp(token, "in")){ +			port->input = 1; +		} +		else if(!strcmp(token, "out")){ +			port->input = 0; +		} +		else if(!strcmp(token, "midi")){ +			port->type = port_midi; +		} +		else if(!strcmp(token, "osc")){ +			port->type = port_osc; +		} +		else if(!strcmp(token, "cv")){ +			port->type = port_cv; +		} +		else if(!strcmp(token, "max")){ +			token = strtok(NULL, " "); +			if(!token){ +				fprintf(stderr, "jack port %s configuration missing argument\n", port->name); +				return 1; +			} +			port->max = strtod(token, NULL); +		} +		else if(!strcmp(token, "min")){ +			token = strtok(NULL, " "); +			if(!token){ +				fprintf(stderr, "jack port %s configuration missing argument\n", port->name); +				return 1; +			} +			port->min = strtod(token, NULL); +		} +		else{ +			fprintf(stderr, "Unknown jack channel configuration token %s on port %s\n", token, port->name); +			return 1; +		} +	} + +	if(port->type == port_none){ +		fprintf(stderr, "jack channel %s assigned no port type\n", port->name); +		return 1; +	} +	return 0; +} + +static int mmjack_configure_instance(instance* inst, char* option, char* value){ +	mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; +	size_t p; + +	if(!strcmp(option, "name")){ +		if(data->client_name){ +			free(data->client_name); +		} +		data->client_name = strdup(value); +		return 0; +	} +	else if(!strcmp(option, "server")){ +		if(data->server_name){ +			free(data->server_name); +		} +		data->server_name = strdup(value); +		return 0; +	} + +	//register new port, first check for unique name +	for(p = 0; p < data->ports; p++){ +		if(!strcmp(data->port[p].name, option)){ +			fprintf(stderr, "jack instance %s has duplicate port %s\n", inst->name, option); +			return 1; +		} +	} +	if(strchr(option, '.')){ +		fprintf(stderr, "Invalid jack channel spec %s.%s\n", inst->name, option); +	} + +	//add port to registry +	//TODO for OSC ports we need to configure subchannels for each message +	data->port = realloc(data->port, (data->ports + 1) * sizeof(mmjack_port)); +	if(!data->port){ +		fprintf(stderr, "Failed to allocate memory\n"); +		return 1; +	} +	data->port[data->ports].name = strdup(option); +	if(!data->port[data->ports].name){ +		fprintf(stderr, "Failed to allocate memory\n"); +		return 1; +	} +	if(mmjack_parse_portconfig(data->port + p, value)){ +		return 1; +	} +	data->ports++; +	return 0; +} + +static instance* mmjack_instance(){ +	instance* inst = mm_instance(); +	if(!inst){ +		return NULL; +	} + +	inst->impl = calloc(1, sizeof(mmjack_instance_data)); +	if(!inst->impl){ +		fprintf(stderr, "Failed to allocate memory\n"); +		return NULL; +	} + +	return inst; +} + +static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){ +	char* next_token = NULL; + +	if(!strncmp(spec, "ch", 2)){ +		next_token = spec + 2; +		if(!strncmp(spec, "channel", 7)){ +			next_token = spec + 7; +		} +	} + +	if(!next_token){ +		fprintf(stderr, "Invalid jack MIDI spec %s\n", spec); +		return 1; +	} + +	ident->fields.sub_channel = strtoul(next_token, &next_token, 10); +	if(ident->fields.sub_channel > 15){ +		fprintf(stderr, "Invalid jack MIDI spec %s, channel out of range\n", spec); +		return 1; +	} + +	if(*next_token != '.'){ +		fprintf(stderr, "Invalid jack MIDI spec %s\n", spec); +		return 1; +	} + +	next_token++; + +	if(!strncmp(next_token, "cc", 2)){ +		ident->fields.sub_type = midi_cc; +		next_token += 2; +	} +	else if(!strncmp(next_token, "note", 4)){ +		ident->fields.sub_type = midi_note; +		next_token += 4; +	} +	else if(!strncmp(next_token, "pressure", 8)){ +		ident->fields.sub_type = midi_pressure; +		next_token += 8; +	} +	else if(!strncmp(next_token, "pitch", 5)){ +		ident->fields.sub_type = midi_pitchbend; +	} +	else if(!strncmp(next_token, "aftertouch", 10)){ +		ident->fields.sub_type = midi_aftertouch; +	} +	else{ +		fprintf(stderr, "Unknown jack MIDI control type in spec %s\n", spec); +		return 1; +	} + +	ident->fields.sub_control = strtoul(next_token, NULL, 10); + +	if(ident->fields.sub_type == midi_none +			|| ident->fields.sub_control > 127){ +		fprintf(stderr, "Invalid jack MIDI spec %s\n", spec); +		return 1; +	} +	return 0; +} + +static channel* mmjack_channel(instance* inst, char* spec, uint8_t flags){ +	mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; +	mmjack_channel_ident ident = { +		.label = 0 +	}; +	size_t u; + +	for(u = 0; u < data->ports; u++){ +		if(!strncmp(spec, data->port[u].name, strlen(data->port[u].name)) +				&& (spec[strlen(data->port[u].name)] == '.' || spec[strlen(data->port[u].name)] == 0)){ +			ident.fields.port = u; +			break; +		} +	} + +	if(u == data->ports){ +		fprintf(stderr, "jack port %s.%s not found\n", inst->name, spec); +		return NULL; +	} + +	if(data->port[u].type == port_midi){ +		//parse midi subspec +		if(!spec[strlen(data->port[u].name)] +				|| mmjack_parse_midispec(&ident, spec + strlen(data->port[u].name) + 1)){ +			return NULL; +		} +	} +	else if(data->port[u].type == port_osc){ +		//TODO parse osc subspec +	} + +	return mm_channel(inst, ident.label, 1); +} + +static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v){ +	mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; +	mmjack_channel_ident ident = { +		.label = 0 +	}; +	size_t u; +	double range; +	uint16_t value; + +	for(u = 0; u < num; u++){ +		ident.label = c[u]->ident; + +		if(data->port[ident.fields.port].input){ +			fprintf(stderr, "jack port %s.%s is an input port, no output is possible\n", inst->name, data->port[ident.fields.port].name); +			continue; +		} +		range = data->port[ident.fields.port].max - data->port[ident.fields.port].min; + +		pthread_mutex_lock(&data->port[ident.fields.port].lock); +		switch(data->port[ident.fields.port].type){ +			case port_cv: +				//scale value to given range +				data->port[ident.fields.port].last = (range * v[u].normalised) + data->port[ident.fields.port].min; +				DBGPF("CV port %s updated to %f\n", data->port[ident.fields.port].name, data->port[ident.fields.port].last); +				break; +			case port_midi: +				value = v[u].normalised * 127.0; +				if(ident.fields.sub_type == midi_pitchbend){ +					value = ((uint16_t)(v[u].normalised * 16384.0)); +				} +				if(mmjack_midiqueue_append(data->port + ident.fields.port, ident, value)){ +					pthread_mutex_unlock(&data->port[ident.fields.port].lock); +					return 1; +				} +				break; +			default: +				fprintf(stderr, "No handler implemented for jack port type %s.%s\n", inst->name, data->port[ident.fields.port].name); +				break; +		} +		pthread_mutex_unlock(&data->port[ident.fields.port].lock); +	} + +	return 0; +} + +static void mmjack_handle_midi(instance* inst, size_t index, mmjack_port* port){ +	size_t u; +	channel* chan = NULL; +	channel_value val; + +	for(u = 0; u < port->queue_len; u++){ +		port->queue[u].ident.fields.port = index; +		chan = mm_channel(inst, port->queue[u].ident.label, 0); +		if(chan){ +			if(port->queue[u].ident.fields.sub_type == midi_pitchbend){ +				val.normalised = ((double)port->queue[u].raw) / 16384.0; +			} +			else{ +				val.normalised = ((double)port->queue[u].raw) / 127.0; +			} +			DBGPF("Pushing MIDI channel %d type %02X control %d value %f raw %d label %" PRIu64 "\n", +					port->queue[u].ident.fields.sub_channel, +					port->queue[u].ident.fields.sub_type, +					port->queue[u].ident.fields.sub_control, +					val.normalised, +					port->queue[u].raw, +					port->queue[u].ident.label); +			if(mm_channel_event(chan, val)){ +				fprintf(stderr, "Failed to push MIDI event to core on jack port %s.%s\n", inst->name, port->name); +			} +		} +	} + +	if(port->queue_len){ +		DBGPF("Pushed %" PRIsize_t " MIDI events to core for jack port %s.%s\n", port->queue_len, inst->name, port->name); +	} +	port->queue_len = 0; +} + +static void mmjack_handle_cv(instance* inst, size_t index, mmjack_port* port){ +	mmjack_channel_ident ident = { +		.fields.port = index +	}; +	double range; +	channel_value val; + +	channel* chan = mm_channel(inst, ident.label, 0); +	if(!chan){ +		//this might happen if a channel is registered but not mapped +		DBGPF("Failed to match jack CV channel %s.%s to core channel\n", inst->name, port->name); +		return; +	} + +	//normalize value +	range = port->max - port->min; +	val.normalised = port->last - port->min; +	val.normalised /= range; +	val.normalised = clamp(val.normalised, 1.0, 0.0); +	DBGPF("Pushing CV channel %s value %f raw %f min %f max %f\n", port->name, val.normalised, port->last, port->min, port->max); +	if(mm_channel_event(chan, val)){ +		fprintf(stderr, "Failed to push CV event to core for %s.%s\n", inst->name, port->name); +	} +} + +static int mmjack_handle(size_t num, managed_fd* fds){ +	size_t u, p; +	instance* inst = NULL; +	mmjack_instance_data* data = NULL; +	ssize_t bytes; +	uint8_t recv_buf[1024]; + +	if(num){ +		for(u = 0; u < num; u++){ +			inst = (instance*) fds[u].impl; +			data = (mmjack_instance_data*) inst->impl; +			bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); +			if(bytes < 0){ +				fprintf(stderr, "Failed to receive on feedback socket for instance %s\n", inst->name); +				return 1; +			} + +			for(p = 0; p < data->ports; p++){ +				if(data->port[p].input && data->port[p].mark){ +					pthread_mutex_lock(&data->port[p].lock); +					switch(data->port[p].type){ +						case port_cv: +							mmjack_handle_cv(inst, p, data->port + p); +							break; +						case port_midi: +							mmjack_handle_midi(inst, p, data->port + p); +							break; +						default: +							fprintf(stderr, "Output handler not implemented for unknown jack channel type on %s.%s\n", inst->name, data->port[p].name); +							break; +					} + +					data->port[p].mark = 0; +					pthread_mutex_unlock(&data->port[p].lock); +				} +			} +		} +	} +	 +	if(config.jack_shutdown){ +		fprintf(stderr, "JACK server disconnected\n"); +		return 1; +	} +	return 0; +} + +static int mmjack_start(){ +	int rv = 1, feedback_fd[2]; +	size_t n, u, p; +	instance** inst = NULL; +	pthread_mutexattr_t mutex_attr; +	mmjack_instance_data* data = NULL; +	jack_status_t error; + +	//set jack logging functions +	jack_set_error_function(mmjack_message_ignore); +	if(config.verbosity & 1){ +		jack_set_error_function(mmjack_message_print); +	} +	jack_set_info_function(mmjack_message_ignore); +	if(config.verbosity & 2){ +		jack_set_info_function(mmjack_message_print); +	} + +	//prepare mutex attributes because the initializer macro for adaptive mutexes is a GNU extension... +	if(pthread_mutexattr_init(&mutex_attr) +			|| pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ADAPTIVE_NP)){ +		fprintf(stderr, "Failed to initialize mutex attributes\n"); +		goto bail; +	} + +	//fetch all instances +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		goto bail; +	} + +	for(u = 0; u < n; u++){ +		data = (mmjack_instance_data*) inst[u]->impl; + +		//connect to the jack server +		data->client = jack_client_open(data->client_name ? data->client_name : JACK_DEFAULT_CLIENT_NAME, +				JackServerName | JackNoStartServer, +				&error, +				data->server_name ? data->server_name : JACK_DEFAULT_SERVER_NAME); + +		if(!data->client){ +			//TODO pretty-print failures +			fprintf(stderr, "jack backend failed to connect to server, return status %u\n", error); +			goto bail; +		} + +		//set up the feedback fd +		if(socketpair(AF_LOCAL, SOCK_DGRAM, 0, feedback_fd)){ +			fprintf(stderr, "Failed to create feedback socket pair\n"); +			goto bail; +		} + +		data->fd = feedback_fd[0]; +		if(mm_manage_fd(feedback_fd[1], BACKEND_NAME, 1, inst[u])){ +			fprintf(stderr, "jack backend failed to register feedback fd with core\n"); +			goto bail; +		} + +		//connect jack callbacks +		jack_set_process_callback(data->client, mmjack_process, inst[u]); +		jack_on_shutdown(data->client, mmjack_server_shutdown, inst[u]); + +		fprintf(stderr, "jack instance %s assigned client name %s\n", inst[u]->name, jack_get_client_name(data->client)); + +		//create and initialize jack ports +		for(p = 0; p < data->ports; p++){ +			if(pthread_mutex_init(&(data->port[p].lock), &mutex_attr)){ +				fprintf(stderr, "Failed to create port mutex\n"); +				goto bail; +			} + +			data->port[p].port = jack_port_register(data->client, +					data->port[p].name, +					(data->port[p].type == port_cv) ? JACK_DEFAULT_AUDIO_TYPE : JACK_DEFAULT_MIDI_TYPE, +					data->port[p].input ? JackPortIsInput : JackPortIsOutput, +					0); + +			jack_set_property(data->client, jack_port_uuid(data->port[p].port), JACKEY_SIGNAL_TYPE, "CV", "text/plain"); + +			if(!data->port[p].port){ +				fprintf(stderr, "Failed to create jack port %s.%s\n", inst[u]->name, data->port[p].name); +				goto bail; +			} +		} + +		//do the thing +		if(jack_activate(data->client)){ +			fprintf(stderr, "Failed to activate jack client for instance %s\n", inst[u]->name); +			goto bail; +		} +	} + +	fprintf(stderr, "jack backend registered %" PRIsize_t " descriptors to core\n", n); +	rv = 0; +bail: +	pthread_mutexattr_destroy(&mutex_attr); +	free(inst); +	return rv; +} + +static int mmjack_shutdown(){ +	size_t n, u, p; +	instance** inst = NULL; +	mmjack_instance_data* data = NULL; + +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		return 1; +	} + +	for(u = 0; u < n; u++){ +		data = (mmjack_instance_data*) inst[u]->impl; + +		//deactivate client to stop processing before free'ing channel data +		if(data->client){ +			jack_deactivate(data->client); +		} + +		//iterate and close ports +		for(p = 0; p < data->ports; p++){ +			jack_remove_property(data->client, jack_port_uuid(data->port[p].port), JACKEY_SIGNAL_TYPE); +			if(data->port[p].port){ +				jack_port_unregister(data->client, data->port[p].port); +			} +			free(data->port[p].name); +			data->port[p].name = NULL; + +			free(data->port[p].queue); +			data->port[p].queue = NULL; +			data->port[p].queue_alloc = data->port[p].queue_len = 0; + +			pthread_mutex_destroy(&data->port[p].lock); +		} + +		//terminate jack connection +		if(data->client){ +			jack_client_close(data->client); +		} + +		//clean up instance data +		free(data->server_name); +		data->server_name = NULL; +		free(data->client_name); +		data->client_name = NULL; +		close(data->fd); +		data->fd = -1; +	} + +	free(inst); + +	fprintf(stderr, "jack backend shut down\n"); +	return 0; +} diff --git a/backends/jack.h b/backends/jack.h new file mode 100644 index 0000000..a7f3e8b --- /dev/null +++ b/backends/jack.h @@ -0,0 +1,76 @@ +#include "midimonster.h" +#include <jack/jack.h> +#include <pthread.h> + +MM_PLUGIN_API int init(); +static int mmjack_configure(char* option, char* value); +static int mmjack_configure_instance(instance* inst, char* option, char* value); +static instance* mmjack_instance(); +static channel* mmjack_channel(instance* inst, char* spec, uint8_t flags); +static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v); +static int mmjack_handle(size_t num, managed_fd* fds); +static int mmjack_start(); +static int mmjack_shutdown(); + +#define JACK_DEFAULT_CLIENT_NAME "MIDIMonster" +#define JACK_DEFAULT_SERVER_NAME "default" +#define JACK_MIDIQUEUE_CHUNK 10 + +enum /*mmjack_midi_channel_type*/ { +	midi_none = 0, +	midi_note = 0x90, +	midi_cc = 0xB0, +	midi_pressure = 0xA0, +	midi_aftertouch = 0xD0, +	midi_pitchbend = 0xE0 +}; + +typedef union { +	struct { +		uint32_t port; +		uint8_t pad; +		uint8_t sub_type; +		uint8_t sub_channel; +		uint8_t sub_control; +	} fields; +	uint64_t label; +} mmjack_channel_ident; + +typedef enum /*_mmjack_port_type*/ { +	port_none = 0, +	port_midi, +	port_osc, +	port_cv +} mmjack_port_type; + +typedef struct /*_mmjack_midiqueue_entry*/ { +	mmjack_channel_ident ident; +	uint16_t raw; +} mmjack_midiqueue; + +typedef struct /*_mmjack_port_data*/ { +	char* name; +	mmjack_port_type type; +	uint8_t input; +	jack_port_t* port; + +	double max; +	double min; +	uint8_t mark; +	double last; +	size_t queue_len; +	size_t queue_alloc; +	mmjack_midiqueue* queue; + +	pthread_mutex_t lock; +} mmjack_port; + +typedef struct /*_jack_instance_data*/ { +	char* server_name; +	char* client_name; +	int fd; + +	jack_client_t* client; +	size_t ports; +	mmjack_port* port; +} mmjack_instance_data; diff --git a/backends/jack.md b/backends/jack.md new file mode 100644 index 0000000..b6ff5a9 --- /dev/null +++ b/backends/jack.md @@ -0,0 +1,84 @@ +### The `jack` backend + +This backend provides read-write access to the JACK Audio Connection Kit low-latency audio transport server for the +transport of control data via either JACK midi ports or control voltage (CV) inputs and outputs. + +#### Global configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `debug`	| `on`			| `off`			| Print `info` level notices from the JACK connection	| +| `errors`      | `on`                  | `off`                 | Print `error` level notices from the JACK connection	| + +#### Instance configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `name`	| `Controller`		| `MIDIMonster`		| Client name for the JACK connection | +| `server`	| `jackserver`		| `default`		| JACK server identifier to connect to | + +Channels (corresponding to JACK ports) need to be configured with their type and, if applicable, value limits. +To configure a port, specify it in the instance configuration using the following syntax: + +``` +port_name = <type> <direction> min <minimum> max <maximum> +``` + +Port names may be any string except for the instance configuration keywords `name` and `server`. + +The following `type` values are currently supported: + +* `midi`: JACK MIDI port for transmitting MIDI event messages +* `cv`: JACK audio port for transmitting DC offset "control voltage" samples (requires `min`/`max` configuration) + +`direction` may be one of `in` or `out`, as seen from the perspective of the MIDIMonster core, thus +`in` means data is being read from the JACK server and `out` transfers data into the JACK server. + +The following example instance configuration would create a MIDI port sending data into JACK, a control voltage output +sending data between `-1` and `1`, and a control voltage input receiving data with values between `0` and `10`. + +``` +midi_out = midi out +cv_out = cv out min -1 max 1 +cv_in = cv in min 0.0 max 10.0 +``` + +Input CV samples outside the configured range will be clipped. The MIDIMonster will not generate output CV samples +outside of the configured range. + +#### Channel specification + +CV ports are exposed as single MIDIMonster channel and directly map to their normalised values. + +MIDI ports provide subchannels for the various MIDI controls available. Each MIDI port carries +16 MIDI channels (numbered 0 through 15), each of which has 128 note controls (numbered 0 through 127), +corresponding pressure controls for each note, 128 control change (CC) controls (numbered likewise), +one channel wide "aftertouch" control and one channel-wide pitchbend control. + +A MIDI port subchannel 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 following values are recognized for `type`: + +* `cc` - Control Changes +* `note` - Note On/Off messages +* `pressure` - Note pressure/aftertouch messages +* `aftertouch` - Channel-wide aftertouch messages +* `pitch` - Channel pitchbend messages + +The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`. + +Example mappings: +``` +jack1.cv_in > jack1.midi_out.ch0.note3 +jack1.midi_in.ch0.pitch > jack1.cv_out +``` + +The MIDI subchannel syntax is intentionally kept compatible to the different MIDI backends also supported +by the MIDIMonster + +#### Known bugs / problems + +While JACK has rudimentary capabilities for transporting OSC messages, configuring and parsing such channels +with this backend would take a great amount of dedicated syntax & code. CV ports can provide fine-grained single +control channels as an alternative to MIDI. This feature may be implemented at some point in the future. diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c new file mode 100644 index 0000000..ccbeb52 --- /dev/null +++ b/backends/libmmbackend.c @@ -0,0 +1,583 @@ +#include "libmmbackend.h" + +void mmbackend_parse_hostspec(char* spec, char** host, char** port){ +	size_t u = 0; + +	if(!spec || !host || !port){ +		return; +	} + +	*port = NULL; + +	//skip leading spaces +	for(; spec[u] && isspace(spec[u]); u++){ +	} + +	if(!spec[u]){ +		*host = NULL; +		return; +	} + +	*host = spec + u; + +	//scan until string end or space +	for(; spec[u] && !isspace(spec[u]); u++){ +	} + +	//if space, the rest should be the port +	if(spec[u]){ +		spec[u] = 0; +		*port = spec + u + 1; +	} +} + +int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ +	struct addrinfo* head; +	struct addrinfo hints = { +		.ai_family = AF_UNSPEC +	}; + +	int error = getaddrinfo(host, port, &hints, &head); +	if(error || !head){ +		fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); +		return 1; +	} + +	memcpy(addr, head->ai_addr, head->ai_addrlen); +	if(len){ +		*len = head->ai_addrlen; +	} + +	freeaddrinfo(head); +	return 0; +} + +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast){ +	int fd = -1, status, yes = 1; +	struct addrinfo hints = { +		.ai_family = AF_UNSPEC, +		.ai_socktype = socktype, +		.ai_flags = (listener ? AI_PASSIVE : 0) +	}; +	struct addrinfo *info, *addr_it; + +	status = getaddrinfo(host, port, &hints, &info); +	if(status){ +		fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(status)); +		return -1; +	} + +	//traverse the result list +	for(addr_it = info; addr_it; addr_it = addr_it->ai_next){ +		fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); +		if(fd < 0){ +			continue; +		} + +		//set required socket options +		yes = 1; +		if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ +			fprintf(stderr, "Failed to enable SO_REUSEADDR on socket\n"); +		} + +		if(mcast){ +			yes = 1; +			if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ +				fprintf(stderr, "Failed to enable SO_BROADCAST on socket\n"); +			} + +			yes = 0; +			if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ +				fprintf(stderr, "Failed to disable IP_MULTICAST_LOOP on socket: %s\n", strerror(errno)); +			} +		} + +		if(listener){ +			status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); +			if(status < 0){ +				close(fd); +				continue; +			} +		} +		else{ +			status = connect(fd, addr_it->ai_addr, addr_it->ai_addrlen); +			if(status < 0){ +				close(fd); +				continue; +			} +		} + +		break; +	} +	freeaddrinfo(info); + +	if(!addr_it){ +		fprintf(stderr, "Failed to create socket for %s port %s\n", host, port); +		return -1; +	} + +	//set nonblocking +	#ifdef _WIN32 +	u_long mode = 1; +	if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){ +		closesocket(fd); +		return 1; +	} +	#else +	int flags = fcntl(fd, F_GETFL, 0); +	if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ +		fprintf(stderr, "Failed to set socket nonblocking\n"); +		close(fd); +		return -1; +	} +	#endif + +	return fd; +} + +int mmbackend_send(int fd, uint8_t* data, size_t length){ +	ssize_t total = 0, sent; +	while(total < length){ +		sent = send(fd, data + total, length - total, 0); +		if(sent < 0){ +			fprintf(stderr, "Failed to send: %s\n", strerror(errno)); +			return 1; +		} +		total += sent; +	} +	return 0; +} + +int mmbackend_send_str(int fd, char* data){ +	return mmbackend_send(fd, (uint8_t*) data, strlen(data)); +} + +json_type json_identify(char* json, size_t length){ +	size_t n; + +	//skip leading blanks +	for(n = 0; json[n] && n < length && isspace(json[n]); n++){ +	} + +	if(n == length){ +		return JSON_INVALID; +	} + +	switch(json[n]){ +		case '{': +			return JSON_OBJECT; +		case '[': +			return JSON_ARRAY; +		case '"': +			return JSON_STRING; +		case '-': +		case '+': +			return JSON_NUMBER; +		default: +			//true false null number +			if(!strncmp(json + n, "true", 4) +					|| !strncmp(json + n, "false", 5)){ +				return JSON_BOOL; +			} +			else if(!strncmp(json + n, "null", 4)){ +				return JSON_NULL; +			} +			//a bit simplistic but it should do +			if(isdigit(json[n])){ +				return JSON_NUMBER; +			} +	} +	return JSON_INVALID; +} + +size_t json_validate(char* json, size_t length){ +	switch(json_identify(json, length)){ +		case JSON_STRING: +			return json_validate_string(json, length); +		case JSON_ARRAY: +			return json_validate_array(json, length); +		case JSON_OBJECT: +			return json_validate_object(json, length); +		case JSON_INVALID: +			return 0; +		default: +			return json_validate_value(json, length); +	} +} + +size_t json_validate_string(char* json, size_t length){ +	size_t string_length = 0, offset; + +	//skip leading whitespace +	for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){ +	} + +	if(offset == length || json[offset] != '"'){ +		return 0; +	} + +	//find terminating quotation mark not preceded by escape +	for(string_length = 1; offset + string_length < length +			&& isprint(json[offset + string_length]) +			&& (json[offset + string_length] != '"' || json[offset + string_length - 1] == '\\'); string_length++){ +	} + +	//complete string found +	if(json[offset + string_length] == '"' && json[offset + string_length - 1] != '\\'){ +		return offset + string_length + 1; +	} + +	return 0; +} + +size_t json_validate_array(char* json, size_t length){ +	size_t offset = 0; + +	//skip leading whitespace +	for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){ +	} + +	if(offset == length || json[offset] != '['){ +		return 0; +	} + +	for(offset++; offset < length; offset++){ +		offset += json_validate(json + offset, length - offset); + +		//skip trailing whitespace, find terminator +		for(; offset < length && isspace(json[offset]); offset++){ +		} + +		if(json[offset] == ','){ +			continue; +		} + +		if(json[offset] == ']'){ +			return offset + 1; +		} + +		break; +	} + +	return 0; +} + +size_t json_validate_object(char* json, size_t length){ +	size_t offset = 0; + +	//skip whitespace +	for(offset = 0; json[offset] && isspace(json[offset]); offset++){ +	} + +	if(offset == length || json[offset] != '{'){ +		return 0; +	} + +	for(offset++; offset < length; offset++){ +		if(json_identify(json + offset, length - offset) != JSON_STRING){ +			//still could be an empty object... +			for(; offset < length && isspace(json[offset]); offset++){ +			} +			if(json[offset] == '}'){ +				return offset + 1; +			} +			return 0; +		} +		offset += json_validate(json + offset, length - offset); + +		//find value separator +		for(; offset < length && isspace(json[offset]); offset++){ +		} + +		if(json[offset] != ':'){ +			return 0; +		} + +		offset++; +		offset += json_validate(json + offset, length - offset); + +		//skip trailing whitespace +		for(; json[offset] && isspace(json[offset]); offset++){ +		} + +		if(json[offset] == '}'){ +			return offset + 1; +		} +		else if(json[offset] != ','){ +			return 0; +		} +	} +	return 0; +} + +size_t json_validate_value(char* json, size_t length){ +	size_t offset = 0, value_length; + +	//skip leading whitespace +	for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){ +	} + +	if(offset == length){ +		return 0; +	} + +	//match complete values +	if(length - offset >= 4 && !strncmp(json + offset, "null", 4)){ +		return offset + 4; +	} +	else if(length - offset >= 4 && !strncmp(json + offset, "true", 4)){ +		return offset + 4; +	} +	else if(length - offset >= 5 && !strncmp(json + offset, "false", 5)){ +		return offset + 5; +	} + +	if(json[offset] == '-' || isdigit(json[offset])){ +		//json number parsing is dumb. +		for(value_length = 1; offset + value_length < length && +					(isdigit(json[offset + value_length]) +					|| json[offset + value_length] == '+' +					|| json[offset + value_length] == '-' +					|| json[offset + value_length] == '.' +					|| tolower(json[offset + value_length]) == 'e'); value_length++){ +		} + +		if(value_length > 0){ +			return offset + value_length; +		} +	} + +	return 0; +} + +size_t json_obj_offset(char* json, char* key){ +	size_t offset = 0; +	uint8_t match = 0; + +	//skip whitespace +	for(offset = 0; json[offset] && isspace(json[offset]); offset++){ +	} + +	if(json[offset] != '{'){ +		return 0; +	} +	offset++; + +	while(json_identify(json + offset, strlen(json + offset)) == JSON_STRING){ +		//skip to key begin +		for(; json[offset] && json[offset] != '"'; offset++){ +		} + +		if(!strncmp(json + offset + 1, key, strlen(key)) && json[offset + 1 + strlen(key)] == '"'){ +			//key found +			match = 1; +		} + +		offset += json_validate_string(json + offset, strlen(json + offset)); + +		//skip to value separator +		for(; json[offset] && json[offset] != ':'; offset++){ +		} + +		//skip whitespace +		for(offset++; json[offset] && isspace(json[offset]); offset++){ +		} + +		if(match){ +			return offset; +		} + +		//add length of value +		offset += json_validate(json + offset, strlen(json + offset)); + +		//skip trailing whitespace +		for(; json[offset] && isspace(json[offset]); offset++){ +		} + +		if(json[offset] == ','){ +			offset++; +			continue; +		} + +		break; +	} + +	return 0; +} + +size_t json_array_offset(char* json, uint64_t key){ +	size_t offset = 0, index = 0; + +	//skip leading whitespace +	for(offset = 0; json[offset] && isspace(json[offset]); offset++){ +	} + +	if(json[offset] != '['){ +		return 0; +	} + +	for(offset++; index <= key; offset++){ +		//skip whitespace +		for(; json[offset] && isspace(json[offset]); offset++){ +		} + +		if(index == key){ +			return offset; +		} + +		offset += json_validate(json + offset, strlen(json + offset)); + +		//skip trailing whitespace, find terminator +		for(; json[offset] && isspace(json[offset]); offset++){ +		} + +		if(json[offset] != ','){ +			break; +		} +		index++; +	} + +	return 0; +} + +json_type json_obj(char* json, char* key){ +	size_t offset = json_obj_offset(json, key); +	if(offset){ +		return json_identify(json + offset, strlen(json + offset)); +	} +	return JSON_INVALID; +} + +json_type json_array(char* json, uint64_t key){ +	size_t offset = json_array_offset(json, key); +	if(offset){ +		return json_identify(json + offset, strlen(json + offset)); +	} +	return JSON_INVALID; +} + +uint8_t json_obj_bool(char* json, char* key, uint8_t fallback){ +	size_t offset = json_obj_offset(json, key); +	if(offset){ +		if(!strncmp(json + offset, "true", 4)){ +			return 1; +		} +		if(!strncmp(json + offset, "false", 5)){ +			return 0; +		} +	} +	return fallback; +} + +uint8_t json_array_bool(char* json, uint64_t key, uint8_t fallback){ +	size_t offset = json_array_offset(json, key); +	if(offset){ +		if(!strncmp(json + offset, "true", 4)){ +			return 1; +		} +		if(!strncmp(json + offset, "false", 5)){ +			return 0; +		} +	} +	return fallback; +} + +int64_t json_obj_int(char* json, char* key, int64_t fallback){ +	char* next_token = NULL; +	int64_t result; +	size_t offset = json_obj_offset(json, key); +	if(offset){ +		result = strtol(json + offset, &next_token, 10); +		if(next_token != json + offset){ +			return result; +		} +	} +	return fallback; +} + +double json_obj_double(char* json, char* key, double fallback){ +	char* next_token = NULL; +	double result; +	size_t offset = json_obj_offset(json, key); +	if(offset){ +		result = strtod(json + offset, &next_token); +		if(next_token != json + offset){ +			return result; +		} +	} +	return fallback; +} + +int64_t json_array_int(char* json, uint64_t key, int64_t fallback){ +	char* next_token = NULL; +	int64_t result; +	size_t offset = json_array_offset(json, key); +	if(offset){ +		result = strtol(json + offset, &next_token, 10); +		if(next_token != json + offset){ +			return result; +		} +	} +	return fallback; +} + +double json_array_double(char* json, uint64_t key, double fallback){ +	char* next_token = NULL; +	double result; +	size_t offset = json_array_offset(json, key); +	if(offset){ +		result = strtod(json + offset, &next_token); +		if(next_token != json + offset){ +			return result; +		} +	} +	return fallback; +} + +char* json_obj_str(char* json, char* key, size_t* length){ +	size_t offset = json_obj_offset(json, key), raw_length; +	if(offset){ +		raw_length = json_validate_string(json + offset, strlen(json + offset)); +		if(length){ +			*length = raw_length - 2; +		} +		return json + offset + 1; +	} +	return NULL; +} + +char* json_obj_strdup(char* json, char* key){ +	size_t len = 0; +	char* value = json_obj_str(json, key, &len), *rv = NULL; +	if(len){ +		rv = calloc(len + 1, sizeof(char)); +		if(rv){ +			memcpy(rv, value, len); +		} +	} +	return rv; +} + +char* json_array_str(char* json, uint64_t key, size_t* length){ +	size_t offset = json_array_offset(json, key), raw_length; +	if(offset){ +		raw_length = json_validate_string(json + offset, strlen(json + offset)); +		if(length){ +			*length = raw_length - 2; +		} +		return json + offset + 1; +	} +	return NULL; +} + +char* json_array_strdup(char* json, uint64_t key){ +	size_t len = 0; +	char* value = json_array_str(json, key, &len), *rv = NULL; +	if(len){ +		rv = calloc(len + 1, sizeof(char)); +		if(rv){ +			memcpy(rv, value, len); +		} +	} +	return rv; +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h new file mode 100644 index 0000000..5749119 --- /dev/null +++ b/backends/libmmbackend.h @@ -0,0 +1,127 @@ +#include <stdint.h> +#include <stdlib.h> +#include <sys/types.h> +#ifdef _WIN32 +#include <ws2tcpip.h> +//#define close closesocket +#else +#include <sys/socket.h> +#include <netdb.h> +#endif +#include <ctype.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include "../portability.h" + +/*** BACKEND IMPLEMENTATION LIBRARY ***/ + +/** Networking functions **/ + +/*  + * Parse spec as host specification in the form + *	host port + * into its constituent parts. + * Returns offsets into the original string and modifies it. + * Returns NULL in *port if none given. + * Returns NULL in both *port and *host if spec was an empty string. + */ +void mmbackend_parse_hostspec(char* spec, char** host, char** port); + +/*  + * Parse a given host / port combination into a sockaddr_storage + * suitable for usage with connect / sendto + * Returns 0 on success + */ +int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len); + +/*  + * Create a socket of given type and mode for a bind / connect host. + * Returns -1 on failure, a valid file descriptor for the socket on success. + */ +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast); + +/* + * Send arbitrary data over multiple writes if necessary + * Returns 1 on failure, 0 on success. + */ +int mmbackend_send(int fd, uint8_t* data, size_t length); + +/* + * Wraps mmbackend_send for cstrings + */ +int mmbackend_send_str(int fd, char* data); + + +/** JSON parsing **/ + +typedef enum /*_json_types*/ { +	JSON_INVALID = 0, +	JSON_STRING, +	JSON_ARRAY, +	JSON_OBJECT, +	JSON_NUMBER, +	JSON_BOOL, +	JSON_NULL +} json_type; + +/*  + * Try to identify the type of JSON data next in the buffer + * Will access at most the next `length` bytes + */ +json_type json_identify(char* json, size_t length); + +/*  + * Validate that a buffer contains a valid JSON document/data within `length` bytes + * Returns the length of a detected JSON document, 0 otherwise (ie. parse failures) + */ +size_t json_validate(char* json, size_t length); +size_t json_validate_string(char* json, size_t length); +size_t json_validate_array(char* json, size_t length); +size_t json_validate_object(char* json, size_t length); +size_t json_validate_value(char* json, size_t length); + +/*  + * Calculate offset for value of `key` + * Assumes a zero-terminated, validated JSON object / array as input + * Returns offset on success, 0 on failure + */ +size_t json_obj_offset(char* json, char* key); +size_t json_array_offset(char* json, uint64_t key); + +/* + * Check for for a key within a JSON object / index within an array + * Assumes a zero-terminated, validated JSON object / array as input + * Returns type of value + */ +json_type json_obj(char* json, char* key); +json_type json_array(char* json, uint64_t key); + +/* + * Fetch boolean value for an object / array key + * Assumes a zero-terminated, validated JSON object / array as input + */ +uint8_t json_obj_bool(char* json, char* key, uint8_t fallback); +uint8_t json_array_bool(char* json, uint64_t key, uint8_t fallback); + +/* + * Fetch integer/double value for an object / array key + * Assumes a zero-terminated validated JSON object / array as input + */ +int64_t json_obj_int(char* json, char* key, int64_t fallback); +double json_obj_double(char* json, char* key, double fallback); +int64_t json_array_int(char* json, uint64_t key, int64_t fallback); +double json_array_double(char* json, uint64_t key, double fallback); + +/*  + * Fetch a string value for an object / array key + * Assumes a zero-terminated validated JSON object / array as input + * json_*_strdup returns a newly-allocated buffer containing + * only the requested value + */ +char* json_obj_str(char* json, char* key, size_t* length); +char* json_obj_strdup(char* json, char* key); +char* json_array_str(char* json, uint64_t key, size_t* length); +char* json_array_strdup(char* json, uint64_t key); diff --git a/backends/loopback.c b/backends/loopback.c index bb93a1f..41e6f85 100644 --- a/backends/loopback.c +++ b/backends/loopback.c @@ -3,17 +3,17 @@  #define BACKEND_NAME "loopback" -int init(){ +MM_PLUGIN_API int init(){  	backend loopback = {  		.name = BACKEND_NAME, -		.conf = backend_configure, -		.create = backend_instance, -		.conf_instance = backend_configure_instance, -		.channel = backend_channel, -		.handle = backend_set, -		.process = backend_handle, -		.start = backend_start, -		.shutdown = backend_shutdown +		.conf = loopback_configure, +		.create = loopback_instance, +		.conf_instance = loopback_configure_instance, +		.channel = loopback_channel, +		.handle = loopback_set, +		.process = loopback_handle, +		.start = loopback_start, +		.shutdown = loopback_shutdown  	};  	//register backend @@ -24,23 +24,23 @@ int init(){  	return 0;  } -static int backend_configure(char* option, char* value){ +static int loopback_configure(char* option, char* value){  	//intentionally ignored  	return 0;  } -static int backend_configure_instance(instance* inst, char* option, char* value){ +static int loopback_configure_instance(instance* inst, char* option, char* value){  	//intentionally ignored  	return 0;  } -static instance* backend_instance(){ +static instance* loopback_instance(){  	instance* i = mm_instance();  	if(!i){  		return NULL;  	} -	i->impl = calloc(1, sizeof(loopback_instance)); +	i->impl = calloc(1, sizeof(loopback_instance_data));  	if(!i->impl){  		fprintf(stderr, "Failed to allocate memory\n");  		return NULL; @@ -49,9 +49,9 @@ static instance* backend_instance(){  	return i;  } -static channel* backend_channel(instance* inst, char* spec){ +static channel* loopback_channel(instance* inst, char* spec, uint8_t flags){  	size_t u; -	loopback_instance* data = (loopback_instance*) inst->impl; +	loopback_instance_data* data = (loopback_instance_data*) inst->impl;  	//find matching channel  	for(u = 0; u < data->n; u++){ @@ -79,7 +79,7 @@ static channel* backend_channel(instance* inst, char* spec){  	return mm_channel(inst, u, 1);  } -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v){ +static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v){  	size_t n;  	for(n = 0; n < num; n++){  		mm_channel_event(c[n], v[n]); @@ -87,19 +87,19 @@ static int backend_set(instance* inst, size_t num, channel** c, channel_value* v  	return 0;  } -static int backend_handle(size_t num, managed_fd* fds){ +static int loopback_handle(size_t num, managed_fd* fds){  	//no events generated here  	return 0;  } -static int backend_start(){ +static int loopback_start(){  	return 0;  } -static int backend_shutdown(){ +static int loopback_shutdown(){  	size_t n, u, p;  	instance** inst = NULL; -	loopback_instance* data = NULL; +	loopback_instance_data* data = NULL;  	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){  		fprintf(stderr, "Failed to fetch instance list\n"); @@ -107,7 +107,7 @@ static int backend_shutdown(){  	}  	for(u = 0; u < n; u++){ -		data = (loopback_instance*) inst[u]->impl; +		data = (loopback_instance_data*) inst[u]->impl;  		for(p = 0; p < data->n; p++){  			free(data->name[p]);  		} @@ -116,5 +116,7 @@ static int backend_shutdown(){  	}  	free(inst); + +	fprintf(stderr, "Loopback backend shut down\n");  	return 0;  } diff --git a/backends/loopback.h b/backends/loopback.h index fe44e91..ee51c66 100644 --- a/backends/loopback.h +++ b/backends/loopback.h @@ -1,16 +1,16 @@  #include "midimonster.h" -int init(); -static int backend_configure(char* option, char* value); -static int backend_configure_instance(instance* instance, char* option, char* value); -static instance* backend_instance(); -static channel* backend_channel(instance* instance, char* spec); -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v); -static int backend_handle(size_t num, managed_fd* fds); -static int backend_start(); -static int backend_shutdown(); +MM_PLUGIN_API int init(); +static int loopback_configure(char* option, char* value); +static int loopback_configure_instance(instance* inst, char* option, char* value); +static instance* loopback_instance(); +static channel* loopback_channel(instance* inst, char* spec, uint8_t flags); +static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v); +static int loopback_handle(size_t num, managed_fd* fds); +static int loopback_start(); +static int loopback_shutdown();  typedef struct /*_loopback_instance_data*/ {  	size_t n;  	char** name; -} loopback_instance; +} loopback_instance_data; diff --git a/backends/loopback.md b/backends/loopback.md new file mode 100644 index 0000000..a06c768 --- /dev/null +++ b/backends/loopback.md @@ -0,0 +1,28 @@ +### The `loopback` backend + +This backend allows the user to create logical mapping channels, for example to exchange triggering +channels easier later. All events that are input are immediately output again on the same channel. + +#### Global configuration + +All global configuration is ignored. + +#### Instance configuration + +All instance configuration is ignored + +#### Channel specification + +A channel may have any string for a name. + +Example mapping: +``` +loop.foo < loop.bar123 +``` + +#### Known bugs / problems + +It is possible (and very easy) to configure loops using this backend. Triggering a loop +will create a deadlock, preventing any other backends from generating events. +Be careful with bidirectional channel mappings, as any input will be immediately +output to the same channel again.
\ No newline at end of file diff --git a/backends/lua.c b/backends/lua.c new file mode 100644 index 0000000..40e6613 --- /dev/null +++ b/backends/lua.c @@ -0,0 +1,510 @@ +#include "lua.h" + +#include <string.h> +#include <unistd.h> +#include <errno.h> +#ifdef MMBACKEND_LUA_TIMERFD +#include <sys/timerfd.h> +#endif + +#define BACKEND_NAME "lua" +#define LUA_REGISTRY_KEY "_midimonster_lua_instance" + +static size_t timers = 0; +static lua_timer* timer = NULL; +uint64_t timer_interval = 0; +#ifdef MMBACKEND_LUA_TIMERFD +static int timer_fd = -1; +#else +static uint64_t last_timestamp; +#endif + +MM_PLUGIN_API int init(){ +	backend lua = { +		#ifndef MMBACKEND_LUA_TIMERFD +		.interval = lua_interval, +		#endif +		.name = BACKEND_NAME, +		.conf = lua_configure, +		.create = lua_instance, +		.conf_instance = lua_configure_instance, +		.channel = lua_channel, +		.handle = lua_set, +		.process = lua_handle, +		.start = lua_start, +		.shutdown = lua_shutdown +	}; + +	//register backend +	if(mm_backend_register(lua)){ +		fprintf(stderr, "Failed to register lua backend\n"); +		return 1; +	} + +	#ifdef MMBACKEND_LUA_TIMERFD +	//create the timer to expire intervals +	timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); +	if(timer_fd < 0){ +		fprintf(stderr, "Failed to create timer for Lua backend\n"); +		return 1; +	} +	#endif +	return 0; +} + +#ifndef MMBACKEND_LUA_TIMERFD +static uint32_t lua_interval(){ +	size_t n = 0; +	uint64_t next_timer = 1000; + +	if(timer_interval){ +		for(n = 0; n < timers; n++){ +			if(timer[n].interval && timer[n].interval - timer[n].delta < next_timer){ +				next_timer = timer[n].interval - timer[n].delta; +			} +		} +		return next_timer; +	} +	return 1000; +} +#endif + +static int lua_update_timerfd(){ +	uint64_t interval = 0, gcd, residual; +	size_t n = 0; +	#ifdef MMBACKEND_LUA_TIMERFD +	struct itimerspec timer_config = { +		0 +	}; +	#endif + +	//find the minimum for the lower interval bounds +	for(n = 0; n < timers; n++){ +		if(timer[n].interval && (!interval || timer[n].interval < interval)){ +			interval = timer[n].interval; +		} +	} + +	//calculate gcd of all timers if any are active +	if(interval){ +		for(n = 0; n < timers; n++){ +			if(timer[n].interval){ +				//calculate gcd of current interval and this timers interval +				gcd = timer[n].interval; +				while(gcd){ +					residual = interval % gcd; +					interval = gcd; +					gcd = residual; +				} +				//since we round everything, 10 is the lowest interval we get +				if(interval == 10){ +					break; +				} +			} +		} + +		#ifdef MMBACKEND_LUA_TIMERFD +		timer_config.it_interval.tv_sec = timer_config.it_value.tv_sec = interval / 1000; +		timer_config.it_interval.tv_nsec = timer_config.it_value.tv_nsec = (interval % 1000) * 1e6; +		#endif +	} + +	if(interval == timer_interval){ +		return 0; +	} + +	#ifdef MMBACKEND_LUA_TIMERFD +	//configure the new interval +	timerfd_settime(timer_fd, 0, &timer_config, NULL); +	#endif +	timer_interval = interval; +	return 0; +} + +static int lua_callback_output(lua_State* interpreter){ +	size_t n = 0; +	channel_value val; +	const char* channel_name = NULL; +	channel* channel = NULL; +	instance* inst = NULL; +	lua_instance_data* data = NULL; + +	if(lua_gettop(interpreter) != 2){ +		fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(interpreter)); +		return 0; +	} + +	//get instance pointer from registry +	lua_pushstring(interpreter, LUA_REGISTRY_KEY); +	lua_gettable(interpreter, LUA_REGISTRYINDEX); +	inst = (instance*) lua_touserdata(interpreter, -1); +	data = (lua_instance_data*) inst->impl; + +	//fetch function parameters +	channel_name = lua_tostring(interpreter, 1); +	val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); + +	//find correct channel & output value +	for(n = 0; n < data->channels; n++){ +		if(!strcmp(channel_name, data->channel_name[n])){ +			channel = mm_channel(inst, n, 0); +			if(!channel){ +				return 0; +			} +			mm_channel_event(channel, val); +			data->output[n] = val.normalised; +			return 0; +		} +	} + +	fprintf(stderr, "Tried to set unknown channel %s.%s\n", inst->name, channel_name); +	return 0; +} + +static int lua_callback_interval(lua_State* interpreter){ +	size_t n = 0; +	uint64_t interval = 0; +	int reference = LUA_NOREF; + +	if(lua_gettop(interpreter) != 2){ +		fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(interpreter)); +		return 0; +	} + +	//get instance pointer from registry +	lua_pushstring(interpreter, LUA_REGISTRY_KEY); +	lua_gettable(interpreter, LUA_REGISTRYINDEX); + +	//fetch and round the interval +	interval = luaL_checkinteger(interpreter, 2); +	if(interval % 10 < 5){ +		interval -= interval % 10; +	} +	else{ +		interval += (10 - (interval % 10)); +	} + +	//push the function again +	lua_pushvalue(interpreter, 1); +	if(lua_gettable(interpreter, LUA_REGISTRYINDEX) == LUA_TNUMBER){ +		//already interval'd +		reference = luaL_checkinteger(interpreter, 4); +	} +	else if(interval){ +		//get a reference to the function +		lua_pushvalue(interpreter, 1); +		reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + +		//the function indexes the reference +		lua_pushvalue(interpreter, 1); +		lua_pushinteger(interpreter, reference); +		lua_settable(interpreter, LUA_REGISTRYINDEX); +	} + +	//find matching timer +	for(n = 0; n < timers; n++){ +		if(timer[n].reference == reference && timer[n].interpreter == interpreter){ +			break; +		} +	} + +	if(n < timers){ +		//set new interval +		timer[n].interval = interval; +		timer[n].delta = 0; +	} +	else if(interval){ +		//append new timer +		timer = realloc(timer, (timers + 1) * sizeof(lua_timer)); +		if(!timer){ +			fprintf(stderr, "Failed to allocate memory\n"); +			timers = 0; +			return 0; +		} +		timer[timers].interval = interval; +		timer[timers].delta = 0; +		timer[timers].interpreter = interpreter; +		timer[timers].reference = reference; +		timers++; +	} + +	//recalculate timerspec +	lua_update_timerfd(); +	return 0; +} + +static int lua_callback_value(lua_State* interpreter, uint8_t input){ +	size_t n = 0; +	instance* inst = NULL; +	lua_instance_data* data = NULL; +	const char* channel_name = NULL; + +	if(lua_gettop(interpreter) != 1){ +		fprintf(stderr, "Lua get_value function called with %d arguments, expected 1 (string)\n", lua_gettop(interpreter)); +		return 0; +	} + +	//get instance pointer from registry +	lua_pushstring(interpreter, LUA_REGISTRY_KEY); +	lua_gettable(interpreter, LUA_REGISTRYINDEX); +	inst = (instance*) lua_touserdata(interpreter, -1); +	data = (lua_instance_data*) inst->impl; + +	//fetch argument +	channel_name = lua_tostring(interpreter, 1); + +	//find correct channel & return value +	for(n = 0; n < data->channels; n++){ +		if(!strcmp(channel_name, data->channel_name[n])){ +			lua_pushnumber(data->interpreter, (input) ? data->input[n] : data->output[n]); +			return 1; +		} +	} + +	fprintf(stderr, "Tried to get unknown channel %s.%s\n", inst->name, channel_name); +	return 0; +} + +static int lua_callback_input_value(lua_State* interpreter){ +	return lua_callback_value(interpreter, 1); +} + +static int lua_callback_output_value(lua_State* interpreter){ +	return lua_callback_value(interpreter, 0); +} + +static int lua_configure(char* option, char* value){ +	fprintf(stderr, "The lua backend does not take any global configuration\n"); +	return 1; +} + +static int lua_configure_instance(instance* inst, char* option, char* value){ +	lua_instance_data* data = (lua_instance_data*) inst->impl; + +	//load a lua file into the interpreter +	if(!strcmp(option, "script") || !strcmp(option, "source")){ +		if(luaL_dofile(data->interpreter, value)){ +			fprintf(stderr, "Failed to load lua source file %s for instance %s: %s\n", value, inst->name, lua_tostring(data->interpreter, -1)); +			return 1; +		} +		return 0; +	} + +	fprintf(stderr, "Unknown configuration parameter %s for lua instance %s\n", option, inst->name); +	return 1; +} + +static instance* lua_instance(){ +	instance* inst = mm_instance(); +	if(!inst){ +		return NULL; +	} + +	lua_instance_data* data = calloc(1, sizeof(lua_instance_data)); +	if(!data){ +		fprintf(stderr, "Failed to allocate memory\n"); +		return NULL; +	} + +	//load the interpreter +	data->interpreter = luaL_newstate(); +	if(!data->interpreter){ +		fprintf(stderr, "Failed to initialize LUA\n"); +		free(data); +		return NULL; +	} +	luaL_openlibs(data->interpreter); + +	//register lua interface functions +	lua_register(data->interpreter, "output", lua_callback_output); +	lua_register(data->interpreter, "interval", lua_callback_interval); +	lua_register(data->interpreter, "input_value", lua_callback_input_value); +	lua_register(data->interpreter, "output_value", lua_callback_output_value); + +	//store instance pointer to the lua state +	lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); +	lua_pushlightuserdata(data->interpreter, (void *) inst); +	lua_settable(data->interpreter, LUA_REGISTRYINDEX); + +	inst->impl = data; +	return inst; +} + +static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ +	size_t u; +	lua_instance_data* data = (lua_instance_data*) inst->impl; + +	//find matching channel +	for(u = 0; u < data->channels; u++){ +		if(!strcmp(spec, data->channel_name[u])){ +			break; +		} +	} + +	//allocate new channel +	if(u == data->channels){ +		data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*)); +		data->reference = realloc(data->reference, (u + 1) * sizeof(int)); +		data->input = realloc(data->input, (u + 1) * sizeof(double)); +		data->output = realloc(data->output, (u + 1) * sizeof(double)); +		if(!data->channel_name || !data->reference || !data->input || !data->output){ +			fprintf(stderr, "Failed to allocate memory\n"); +			return NULL; +		} + +		data->reference[u] = LUA_NOREF; +		data->input[u] = data->output[u] = 0.0; +		data->channel_name[u] = strdup(spec); +		if(!data->channel_name[u]){ +			fprintf(stderr, "Failed to allocate memory\n"); +			return NULL; +		} +		data->channels++; +	} + +	return mm_channel(inst, u, 1); +} + +static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ +	size_t n = 0; +	lua_instance_data* data = (lua_instance_data*) inst->impl; + +	//handle all incoming events +	for(n = 0; n < num; n++){ +		data->input[c[n]->ident] = v[n].normalised; +		//call lua channel handlers if present +		if(data->reference[c[n]->ident] != LUA_NOREF){ +			lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); +			lua_pushnumber(data->interpreter, v[n].normalised); +			if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ +				fprintf(stderr, "Failed to call handler for %s.%s: %s\n", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); +				lua_pop(data->interpreter, 1); +			} +		} +	} +	return 0; +} + +static int lua_handle(size_t num, managed_fd* fds){ +	uint64_t delta = timer_interval; +	size_t n; + +	#ifdef MMBACKEND_LUA_TIMERFD +	uint8_t read_buffer[100]; +	if(!num){ +		return 0; +	} + +	//read the timer iteration to acknowledge the fd +	if(read(timer_fd, read_buffer, sizeof(read_buffer)) < 0){ +		fprintf(stderr, "Failed to read from Lua timer: %s\n", strerror(errno)); +		return 1; +	} +	#else +	if(!last_timestamp){ +		last_timestamp = mm_timestamp(); +	} +	delta = mm_timestamp() - last_timestamp; +	last_timestamp = mm_timestamp(); +	#endif + +	//no timers active +	if(!timer_interval){ +		return 0; +	} + +	//add delta to all active timers +	for(n = 0; n < timers; n++){ +		if(timer[n].interval){ +			timer[n].delta += delta; +			//call lua function if timer expired +			if(timer[n].delta >= timer[n].interval){ +				timer[n].delta %= timer[n].interval; +				lua_rawgeti(timer[n].interpreter, LUA_REGISTRYINDEX, timer[n].reference); +				lua_pcall(timer[n].interpreter, 0, 0, 0); +			} +		} +	} +	return 0; +} + +static int lua_start(){ +	size_t n, u, p; +	instance** inst = NULL; +	lua_instance_data* data = NULL; + +	//fetch all defined instances +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		return 1; +	} + +	//resolve channels to their handler functions +	for(u = 0; u < n; u++){ +		data = (lua_instance_data*) inst[u]->impl; +		for(p = 0; p < data->channels; p++){ +			//exclude reserved names +			if(strcmp(data->channel_name[p], "output") +					&& strcmp(data->channel_name[p], "input_value") +					&& strcmp(data->channel_name[p], "output_value") +					&& strcmp(data->channel_name[p], "interval")){ +				lua_getglobal(data->interpreter, data->channel_name[p]); +				data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); +				if(data->reference[p] == LUA_REFNIL){ +					data->reference[p] = LUA_NOREF; +				} +			} +		} +	} + +	free(inst); + +	#ifdef MMBACKEND_LUA_TIMERFD +	//register the timer with the core +	fprintf(stderr, "Lua backend registering 1 descriptor to core\n"); +	if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ +		return 1; +	} +	#endif +	return 0; +} + +static int lua_shutdown(){ +	size_t n, u, p; +	instance** inst = NULL; +	lua_instance_data* data = NULL; + +	//fetch all instances +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		return 1; +	} + +	for(u = 0; u < n; u++){ +		data = (lua_instance_data*) inst[u]->impl; +		//stop the interpreter +		lua_close(data->interpreter); +		//cleanup channel data +		for(p = 0; p < data->channels; p++){ +			free(data->channel_name[p]); +		} +		free(data->channel_name); +		free(data->reference); +		free(data->input); +		free(data->output); +		free(inst[u]->impl); +	} + +	free(inst); +	//free module-global data +	free(timer); +	timer = NULL; +	timers = 0; +	#ifdef MMBACKEND_LUA_TIMERFD +	close(timer_fd); +	timer_fd = -1; +	#endif + +	fprintf(stderr, "Lua backend shut down\n"); +	return 0; +} diff --git a/backends/lua.h b/backends/lua.h new file mode 100644 index 0000000..4ea5b0a --- /dev/null +++ b/backends/lua.h @@ -0,0 +1,39 @@ +#include "midimonster.h" + +#include <lua.h> +#include <lualib.h> +#include <lauxlib.h> + +//OSX and Windows don't have the cool new toys... +#ifdef __linux__ +	#define MMBACKEND_LUA_TIMERFD +#endif + +MM_PLUGIN_API int init(); +static int lua_configure(char* option, char* value); +static int lua_configure_instance(instance* inst, char* option, char* value); +static instance* lua_instance(); +static channel* lua_channel(instance* inst, char* spec, uint8_t flags); +static int lua_set(instance* inst, size_t num, channel** c, channel_value* v); +static int lua_handle(size_t num, managed_fd* fds); +static int lua_start(); +static int lua_shutdown(); +#ifndef MMBACKEND_LUA_TIMERFD +static uint32_t lua_interval(); +#endif + +typedef struct /*_lua_instance_data*/ { +	size_t channels; +	char** channel_name; +	int* reference; +	double* input; +	double* output; +	lua_State* interpreter; +} lua_instance_data; + +typedef struct /*_lua_interval_callback*/ { +	uint64_t interval; +	uint64_t delta; +	lua_State* interpreter; +	int reference; +} lua_timer; diff --git a/backends/lua.md b/backends/lua.md new file mode 100644 index 0000000..f38e189 --- /dev/null +++ b/backends/lua.md @@ -0,0 +1,66 @@ +### The `lua` backend + +The `lua` backend provides a flexible programming environment, allowing users to route and manipulate +events using the Lua programming language. + +Every instance has it's own interpreter state which can be loaded with custom handler scripts. + +To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) +with the value (as a Lua `number` type) as parameter. + +The following functions are provided within the Lua interpreter for interaction with the MIDIMonster + +| Function			| Usage example			| Description				| +|-------------------------------|-------------------------------|---------------------------------------| +| `output(string, number)`	| `output("foo", 0.75)`		| Output a value event to a channel	| +| `interval(function, number)`	| `interval(update, 100)`	| Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms) | +| `input_value(string)`		| `input_value("foo")`		| Get the last input value on a channel	| +| `output_value(string)`	| `output_value("bar")`		| Get the last output value on a channel | + + +Example script: +``` +function bar(value) +	output("foo", value / 2) +end + +step = 0 +function toggle() +	output("bar", step * 1.0) +	step = (step + 1) % 2; +end + +interval(toggle, 1000) +``` + +Input values range between 0.0 and 1.0, output values are clamped to the same range. + +#### Global configuration + +The `lua` backend does not take any global configuration. + +#### Instance configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `script`	| `script.lua`		| none			| Lua source file (relative to configuration file)| + +A single instance may have multiple `source` options specified, which will all be read cumulatively. + +#### Channel specification + +Channel names may be any valid Lua function name. + +Example mapping: +``` +lua1.foo > lua2.bar +``` + +#### Known bugs / problems + +Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`) as an +input channel name to a Lua instance will not call any handler functions. +Using these names as arguments to the output and value interface functions works as intended. + +Output values will not trigger corresponding input event handlers unless the channel is mapped +back in the MIDIMonster configuration. diff --git a/backends/maweb.c b/backends/maweb.c new file mode 100644 index 0000000..d008cc0 --- /dev/null +++ b/backends/maweb.c @@ -0,0 +1,1072 @@ +#include <string.h> +#include <unistd.h> +#include <errno.h> +#ifndef MAWEB_NO_LIBSSL +#include <openssl/md5.h> +#endif + +#include "libmmbackend.h" +#include "maweb.h" + +#define BACKEND_NAME "maweb" +#define WS_LEN(a) ((a) & 0x7F) +#define WS_OP(a) ((a) & 0x0F) +#define WS_FLAG_FIN 0x80 +#define WS_FLAG_MASK 0x80 + +static uint64_t last_keepalive = 0; +static uint64_t update_interval = 50; +static uint64_t last_update = 0; +static uint64_t updates_inflight = 0; + +static maweb_command_key cmdline_keys[] = { +	{"PREV", 109, 0, 1}, {"SET", 108, 1, 0, 1}, {"NEXT", 110, 0, 1}, +	{"TIME", 58, 1, 1}, {"EDIT", 55, 1, 1}, {"UPDATE", 57, 1, 1}, +	{"OOPS", 53, 1, 1}, {"ESC", 54, 1, 1}, {"CLEAR", 105, 1, 1}, +	{"0", 86, 1, 1}, {"1", 87, 1, 1}, {"2", 88, 1, 1}, +	{"3", 89, 1, 1}, {"4", 90, 1, 1}, {"5", 91, 1, 1}, +	{"6", 92, 1, 1}, {"7", 93, 1, 1}, {"8", 94, 1, 1}, +	{"9", 95, 1, 1}, {"PUNKT", 98, 1, 1}, {"ENTER", 106, 1, 1}, +	{"PLUS", 96, 1, 1}, {"MINUS", 97, 1, 1}, {"THRU", 102, 1, 1}, +	{"IF", 103, 1, 1}, {"AT", 104, 1, 1}, {"FULL", 99, 1, 1}, +	{"MA", 68, 0, 1}, {"HIGH", 100, 1, 1, 1}, {"SOLO", 101, 1, 1, 1}, +	{"SELECT", 42, 1, 1}, {"OFF", 43, 1, 1}, {"ON", 46, 1, 1}, +	{"ASSIGN", 63, 1, 1}, {"LABEL", 0, 1, 1}, +	{"COPY", 73, 1, 1}, {"DELETE", 69, 1, 1}, {"STORE", 59, 1, 1}, +	{"GOTO", 56, 1, 1}, {"PAGE", 70, 1, 1}, {"MACRO", 71, 1, 1}, +	{"PRESET", 72, 1, 1}, {"SEQU", 74, 1, 1}, {"CUE", 75, 1, 1}, +	{"EXEC", 76, 1, 1}, {"FIXTURE", 83, 1, 1}, {"GROUP", 84, 1, 1}, +	{"GO_MINUS", 10, 1, 1}, {"PAUSE", 9, 1, 1}, {"GO_PLUS", 11, 1, 1}, + +	{"FIXTURE_CHANNEL", 0, 1, 1}, {"FIXTURE_GROUP_PRESET", 0, 1, 1}, +	{"EXEC_CUE", 0, 1, 1}, {"STORE_UPDATE", 0, 1, 1}, {"PROG_ONLY", 0, 1, 1, 1}, +	{"SPECIAL_DIALOGUE", 0, 1, 1}, +	{"ODD", 0, 1, 1}, {"EVEN", 0, 1, 1}, +	{"WINGS", 0, 1, 1}, {"RESET", 0, 1, 1}, +	//gma2 internal only +	{"CHPGPLUS", 3}, {"CHPGMINUS", 4}, +	{"FDPGPLUS", 5}, {"FDPGMINUS", 6}, +	{"BTPGPLUS", 7}, {"BTPGMINUS", 8}, +	{"X1", 12}, {"X2", 13}, {"X3", 14}, +	{"X4", 15}, {"X5", 16}, {"X6", 17}, +	{"X7", 18}, {"X8", 19}, {"X9", 20}, +	{"X10", 21}, {"X11", 22}, {"X12", 23}, +	{"X13", 24}, {"X14", 25}, {"X15", 26}, +	{"X16", 27}, {"X17", 28}, {"X18", 29}, +	{"X19", 30}, {"X20", 31}, +	{"V1", 120}, {"V2", 121}, {"V3", 122}, +	{"V4", 123}, {"V5", 124}, {"V6", 125}, +	{"V7", 126}, {"V8", 127}, {"V9", 128}, +	{"V10", 129}, +	{"NIPPLE", 40}, +	{"TOOLS", 119}, {"SETUP", 117}, {"BACKUP", 117}, +	{"BLIND", 60}, {"FREEZE", 61}, {"PREVIEW", 62}, +	{"FIX", 41}, {"TEMP", 44}, {"TOP", 45}, +	{"VIEW", 66}, {"EFFECT", 67}, {"CHANNEL", 82}, +	{"MOVE", 85}, {"BLACKOUT", 65}, +	{"PLEASE", 106}, +	{"LIST", 32}, {"USER1", 33}, {"USER2", 34}, +	{"ALIGN", 64}, {"HELP", 116}, +	{"UP", 107}, {"DOWN", 111}, +	{"FASTREVERSE", 47}, {"LEARN", 48}, {"FASTFORWARD", 49}, +	{"GO_MINUS_SMALL", 50}, {"PAUSE_SMALL", 51}, {"GO_PLUS_SMALL", 52} +}; + +MM_PLUGIN_API int init(){ +	backend maweb = { +		.name = BACKEND_NAME, +		.conf = maweb_configure, +		.create = maweb_instance, +		.conf_instance = maweb_configure_instance, +		.channel = maweb_channel, +		.handle = maweb_set, +		.process = maweb_handle, +		.start = maweb_start, +		.shutdown = maweb_shutdown, +		.interval = maweb_interval +	}; + +	//register backend +	if(mm_backend_register(maweb)){ +		fprintf(stderr, "Failed to register maweb backend\n"); +		return 1; +	} +	return 0; +} + +static ssize_t maweb_channel_index(maweb_instance_data* data, maweb_channel_type type, uint16_t page, uint16_t index){ +	size_t n; +	for(n = 0; n < data->channels; n++){ +		if(data->channel[n].type == type +				&& data->channel[n].page == page +				&& data->channel[n].index == index){ +			return n; +		} +	} +	return -1; +} + +static int channel_comparator(const void* raw_a, const void* raw_b){ +	maweb_channel_data* a = (maweb_channel_data*) raw_a; +	maweb_channel_data* b = (maweb_channel_data*) raw_b; + +	//this needs to take into account command line channels +	//they need to be sorted last so that the channel poll logic works properly +	if(a->page != b->page){ +		return a->page - b->page; +	} +	//execs and their components are sorted by index first, type second +	if(a->type < cmdline && b->type < cmdline){ +		if(a->index != b->index){ +			return a->index - b->index; +		} +		return a->type - b->type; +	} +	//if either one is not an exec, sort by type first, index second +	if(a->type != b->type){ +		return a->type - b->type; +	} +	return a->index - b->index; +} + +static uint32_t maweb_interval(){ +	return update_interval - (last_update % update_interval); +} + +static int maweb_configure(char* option, char* value){ +	if(!strcmp(option, "interval")){ +		update_interval = strtoul(value, NULL, 10); +		return 0; +	} + +	fprintf(stderr, "Unknown maweb backend configuration option %s\n", option); +	return 1; +} + +static int maweb_configure_instance(instance* inst, char* option, char* value){ +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; +	char* host = NULL, *port = NULL; + +	if(!strcmp(option, "host")){ +		mmbackend_parse_hostspec(value, &host, &port); +		if(!host){ +			fprintf(stderr, "Invalid host specified for maweb instance %s\n", inst->name); +			return 1; +		} +		free(data->host); +		data->host = strdup(host); +		free(data->port); +		data->port = NULL; +		if(port){ +			data->port = strdup(port); +		} +		return 0; +	} +	else if(!strcmp(option, "user")){ +		free(data->user); +		data->user = strdup(value); +		return 0; +	} +	else if(!strcmp(option, "password")){ +		#ifndef MAWEB_NO_LIBSSL +		size_t n; +		uint8_t password_hash[MD5_DIGEST_LENGTH]; + +		MD5((uint8_t*) value, strlen(value), (uint8_t*) password_hash); +		data->pass = realloc(data->pass, (2 * MD5_DIGEST_LENGTH + 1) * sizeof(char)); +		for(n = 0; n < MD5_DIGEST_LENGTH; n++){ +			snprintf(data->pass + 2 * n, 3, "%02x", password_hash[n]); +		} +		return 0; +		#else +		fprintf(stderr, "This build of the maweb backend only supports the default password\n"); +		return 1; +		#endif +	} +	else if(!strcmp(option, "cmdline")){ +		if(!strcmp(value, "console")){ +			data->cmdline = cmd_console; +		} +		else if(!strcmp(value, "remote")){ +			data->cmdline = cmd_remote; +		} +		else if(!strcmp(value, "downgrade")){ +			data->cmdline = cmd_downgrade; +		} +		else{ +			fprintf(stderr, "Unknown maweb commandline mode %s for instance %s\n", value, inst->name); +			return 1; +		} +		return 0; +	} + +	fprintf(stderr, "Unknown configuration parameter %s for maweb instance %s\n", option, inst->name); +	return 1; +} + +static instance* maweb_instance(){ +	instance* inst = mm_instance(); +	if(!inst){ +		return NULL; +	} + +	maweb_instance_data* data = calloc(1, sizeof(maweb_instance_data)); +	if(!data){ +		fprintf(stderr, "Failed to allocate memory\n"); +		return NULL; +	} + +	data->fd = -1; +	data->buffer = calloc(MAWEB_RECV_CHUNK, sizeof(uint8_t)); +	if(!data->buffer){ +		fprintf(stderr, "Failed to allocate memory\n"); +		free(data); +		return NULL; +	} +	data->allocated = MAWEB_RECV_CHUNK; + +	inst->impl = data; +	return inst; +} + +static channel* maweb_channel(instance* inst, char* spec, uint8_t flags){ +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; +	maweb_channel_data chan = { +		0 +	}; +	char* next_token = NULL; +	channel* channel_ref = NULL; +	size_t n; + +	if(!strncmp(spec, "page", 4)){ +		chan.page = strtoul(spec + 4, &next_token, 10); +		if(*next_token != '.'){ +			fprintf(stderr, "Failed to parse maweb channel spec %s: Missing separator\n", spec); +			return NULL; +		} + +		next_token++; +		if(!strncmp(next_token, "fader", 5)){ +			chan.type = exec_fader; +			next_token += 5; +		} +		else if(!strncmp(next_token, "upper", 5)){ +			chan.type = exec_upper; +			next_token += 5; +		} +		else if(!strncmp(next_token, "lower", 5)){ +			chan.type = exec_lower; +			next_token += 5; +		} +		else if(!strncmp(next_token, "flash", 5)){ +			chan.type = exec_button; +			next_token += 5; +		} +		else if(!strncmp(next_token, "button", 6)){ +			chan.type = exec_button; +			next_token += 6; +		} +		chan.index = strtoul(next_token, NULL, 10); +	} +	else{ +		for(n = 0; n < sizeof(cmdline_keys) / sizeof(maweb_command_key); n++){ +			if(!strcmp(spec, cmdline_keys[n].name)){ +				if((data->cmdline == cmd_remote && !cmdline_keys[n].press && !cmdline_keys[n].release) +							|| (data->cmdline == cmd_console && !cmdline_keys[n].lua)){ +					fprintf(stderr, "maweb cmdline key %s does not work with the current commandline mode for instance %s\n", spec, inst->name); +					return NULL; +				} + +				chan.type = cmdline; +				chan.index = n + 1; +				chan.page = 1; +				break; +			} +		} +	} + +	if(chan.type && chan.index && chan.page){ +		//actually, those are zero-indexed... +		chan.index--; +		chan.page--; + +		if(maweb_channel_index(data, chan.type, chan.page, chan.index) == -1){ +			data->channel = realloc(data->channel, (data->channels + 1) * sizeof(maweb_channel_data)); +			if(!data->channel){ +				fprintf(stderr, "Failed to allocate memory\n"); +				return NULL; +			} +			data->channel[data->channels] = chan; +			data->channels++; +		} + +		channel_ref = mm_channel(inst, maweb_channel_index(data, chan.type, chan.page, chan.index), 1); +		data->channel[maweb_channel_index(data, chan.type, chan.page, chan.index)].chan = channel_ref; +		return channel_ref; +	} + +	fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec); +	return NULL; +} + +static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload, size_t len){ +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; +	uint8_t frame_header[MAWEB_FRAME_HEADER_LENGTH] = ""; +	size_t header_bytes = 2; +	uint16_t* payload_len16 = (uint16_t*) (frame_header + 2); +	uint64_t* payload_len64 = (uint64_t*) (frame_header + 2); + +	frame_header[0] = WS_FLAG_FIN | op; +	if(len <= 125){ +		frame_header[1] = WS_FLAG_MASK | len; +	} +	else if(len <= 0xFFFF){ +		frame_header[1] = WS_FLAG_MASK | 126; +		*payload_len16 = htobe16(len); +		header_bytes += 2; +	} +	else{ +		frame_header[1] = WS_FLAG_MASK | 127; +		*payload_len64 = htobe64(len); +		header_bytes += 8; +	} +	//send a zero masking key because masking is stupid +	header_bytes += 4; + +	if(mmbackend_send(data->fd, frame_header, header_bytes) +			|| mmbackend_send(data->fd, payload, len)){ +		return 1; +	} + +	return 0; +} + +static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){ +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; +	size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control; +	int64_t exec_index = json_obj_int(payload, "iExec", 191); +	ssize_t channel_index; +	channel_value evt; + +	if(!exec_blocks){ +		if(metatype == 3){ +			//ignore unused buttons +			return 0; +		} +		fprintf(stderr, "maweb missing exec block data on exec %" PRIu64 ".%" PRIu64 "\n", page, exec_index); +		return 1; +	} + +	//the bottomButtons key has an additional subentry +	if(metatype == 3){ +		exec_blocks += json_obj_offset(payload + exec_blocks, "items"); +	} + +	//iterate over executor blocks +	for(offset = json_array_offset(payload + exec_blocks, block); offset; offset = json_array_offset(payload + exec_blocks, block)){ +		control = exec_blocks + offset + json_obj_offset(payload + exec_blocks + offset, "fader"); + +		channel_index = maweb_channel_index(data, exec_fader, page - 1, exec_index); +		if(channel_index >= 0){ +			if(!data->channel[channel_index].input_blocked){ +				evt.normalised = json_obj_double(payload + control, "v", 0.0); +				if(evt.normalised != data->channel[channel_index].in){ +					mm_channel_event(mm_channel(inst, channel_index, 0), evt); +					data->channel[channel_index].in = evt.normalised; +				} +			} +			else{ +				//block input immediately after channel set to prevent feedback loops +				data->channel[channel_index].input_blocked--; +			} +		} + +		channel_index = maweb_channel_index(data, exec_button, page - 1, exec_index); +		if(channel_index >= 0){ +			if(!data->channel[channel_index].input_blocked){ +				evt.normalised = json_obj_int(payload, "isRun", 0); +				if(evt.normalised != data->channel[channel_index].in){ +					mm_channel_event(mm_channel(inst, channel_index, 0), evt); +					data->channel[channel_index].in = evt.normalised; +				} +			} +			else{ +				data->channel[channel_index].input_blocked--; +			} +		} + +		DBGPF("maweb page %" PRIu64 " exec %" PRIu64 " value %f running %" PRIu64 "\n", page, exec_index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); +		exec_index++; +		block++; +	} + +	return 0; +} + +static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, size_t payload_length){ +	size_t base_offset = json_obj_offset(payload, "itemGroups"), group_offset, subgroup_offset, item_offset; +	uint64_t group = 0, subgroup, item, metatype; + +	if(!page){ +		fprintf(stderr, "maweb received playbacks for invalid page\n"); +		return 0; +	} + +	if(!base_offset){ +		fprintf(stderr, "maweb playback data missing item key\n"); +		return 0; +	} + +	//iterate .itemGroups +	for(group_offset = json_array_offset(payload + base_offset, group); +			group_offset; +			group_offset = json_array_offset(payload + base_offset, group)){ +		metatype = json_obj_int(payload + base_offset + group_offset, "itemsType", 0); +		//iterate .itemGroups.items +		//FIXME this is problematic if there is no "items" key +		group_offset = group_offset + json_obj_offset(payload + base_offset + group_offset, "items"); +		if(group_offset){ +			subgroup = 0; +			group_offset += base_offset; +			for(subgroup_offset = json_array_offset(payload + group_offset, subgroup); +					subgroup_offset; +					subgroup_offset = json_array_offset(payload + group_offset, subgroup)){ +				//iterate .itemGroups.items[n] +				item = 0; +				subgroup_offset += group_offset; +				for(item_offset = json_array_offset(payload + subgroup_offset, item); +						item_offset; +						item_offset = json_array_offset(payload + subgroup_offset, item)){ +					maweb_process_playback(inst, page, metatype, +							payload + subgroup_offset + item_offset, +							payload_length - subgroup_offset - item_offset); +					item++; +				} +				subgroup++; +			} +		} +		group++; +	} +	updates_inflight--; +	DBGPF("maweb playback message processing done, %" PRIu64 " updates inflight\n", updates_inflight); +	return 0; +} + +static int maweb_request_playbacks(instance* inst){ +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; +	char xmit_buffer[MAWEB_XMIT_CHUNK]; +	int rv = 0; + +	char item_indices[1024] = "[300,400,500]", item_counts[1024] = "[16,16,16]", item_types[1024] = "[3,3,3]"; +	size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels; + +	if(updates_inflight){ +		fprintf(stderr, "maweb skipping update request, %" PRIu64 " updates still inflight\n", updates_inflight); +		return 0; +	} + +	//only request faders and buttons +	for(channel = 0; channel < data->channels && data->channel[channel].type < cmdline; channel++){ +		offsets[0] = offsets[1] = offsets[2] = 1; +		page_index = data->channel[channel].page; +		//poll logic differs between the consoles because reasons +		//don't quote me on this section +		if(data->peer_type == peer_dot2){ +			//blocks 0, 100 & 200 have 21 execs and need to be queried from fader view +			view = (data->channel[channel].index >= 300) ? 3 : 2; + +			for(channel_offset = 1; channel + channel_offset <= data->channels +					&& data->channel[channel + channel_offset].type < cmdline; channel_offset++){ +				channels = channel + channel_offset - 1; +				//find end for this exec block +				for(; channel + channel_offset < data->channels; channel_offset++){ +					if(data->channel[channel + channel_offset].page != page_index +							|| (data->channel[channels].index / 100) != (data->channel[channel + channel_offset].index / 100)){ +						break; +					} +				} + +				//add request block for the exec block +				offsets[0] += snprintf(item_indices + offsets[0], sizeof(item_indices) - offsets[0], "%d,", data->channel[channels].index); +				offsets[1] += snprintf(item_counts + offsets[1], sizeof(item_counts) - offsets[1], "%d,", data->channel[channel + channel_offset - 1].index - data->channel[channels].index + 1); +				offsets[2] += snprintf(item_types + offsets[2], sizeof(item_types) - offsets[2], "%d,", (data->channel[channels].index < 100) ? 2 : 3); + +				//send on last channel, page boundary, metamode boundary +				if(channel + channel_offset >= data->channels +						|| data->channel[channel + channel_offset].page != page_index +						|| (data->channel[channel].index < 300) != (data->channel[channel + channel_offset].index < 300)){ +					break; +				} +			} + +			//terminate arrays (overwriting the last array separator) +			offsets[0] += snprintf(item_indices + offsets[0] - 1, sizeof(item_indices) - offsets[0], "]"); +			offsets[1] += snprintf(item_counts + offsets[1] - 1, sizeof(item_counts) - offsets[1], "]"); +			offsets[2] += snprintf(item_types + offsets[2] - 1, sizeof(item_types) - offsets[2], "]"); +		} +		else{ +			//for the ma, the view equals the exec type requested (we can query all button execs from button view, all fader execs from fader view) +			view = (data->channel[channel].index >= 100) ? 3 : 2; +			snprintf(item_types, sizeof(item_types), "[%" PRIsize_t "]", view); +			//this channel must be included, so it must be in range for the first startindex +			snprintf(item_indices, sizeof(item_indices), "[%d]", (data->channel[channel].index / 5) * 5); + +			//find end of exec block +			for(channel_offset = 1; channel + channel_offset < data->channels +					&& data->channel[channel].page == data->channel[channel + channel_offset].page +					&& data->channel[channel].index / 100 == data->channel[channel + channel_offset].index / 100; channel_offset++){ +			} + +			//gma execs are grouped in blocks of 5 +			channels = data->channel[channel + channel_offset - 1].index - (data->channel[channel].index / 5) * 5; +			snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5)); +		} + +		DBGPF("maweb poll range first %d: %d.%d last %d: %d.%d next %d: %d.%d\n", +				data->channel[channel].type, data->channel[channel].page, data->channel[channel].index, +				data->channel[channel + channel_offset - 1].type, data->channel[channel + channel_offset - 1].page, data->channel[channel + channel_offset - 1].index, +				data->channel[channel + channel_offset].type, data->channel[channel + channel_offset].page, data->channel[channel + channel_offset].index); + +		//advance base channel +		channel += channel_offset - 1; + +		//send current request +		snprintf(xmit_buffer, sizeof(xmit_buffer), +				"{" +				"\"requestType\":\"playbacks\"," +				"\"startIndex\":%s," +				"\"itemsCount\":%s," +				"\"pageIndex\":%" PRIsize_t "," +				"\"itemsType\":%s," +				"\"view\":%" PRIsize_t "," +				"\"execButtonViewMode\":2,"	//extended +				"\"buttonsViewMode\":0,"	//get vfader for button execs +				"\"session\":%" PRIu64 +				"}", +				item_indices, +				item_counts, +				page_index, +				item_types, +				view, +				data->session); +		rv |= maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); +		DBGPF("maweb poll request: %s\n", xmit_buffer); +		updates_inflight++; +	} + +	DBGPF("maweb poll request handling done, %" PRIu64 " updates requested\n", updates_inflight); +	return rv; +} + +static int maweb_handle_message(instance* inst, char* payload, size_t payload_length){ +	char xmit_buffer[MAWEB_XMIT_CHUNK]; +	char* field; +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; + +	//query this early to save on unnecessary parser passes with stupid-huge data messages +	if(json_obj(payload, "responseType") == JSON_STRING){ +		field = json_obj_str(payload, "responseType", NULL); +		if(!strncmp(field, "login", 5)){ +			if(json_obj_bool(payload, "result", 0)){ +				fprintf(stderr, "maweb login successful\n"); +				data->login = 1; +			} +			else{ +				fprintf(stderr, "maweb login failed\n"); +				data->login = 0; +			} +		} +		if(!strncmp(field, "playbacks", 9)){ +			if(maweb_process_playbacks(inst, json_obj_int(payload, "iPage", 0), payload, payload_length)){ +				fprintf(stderr, "maweb failed to handle/request input data\n"); +			} +			return 0; +		} +	} + +	DBGPF("maweb message (%" PRIsize_t "): %s\n", payload_length, payload); +	if(json_obj(payload, "session") == JSON_NUMBER){ +		data->session = json_obj_int(payload, "session", data->session); +		if(data->session < 0){ +				fprintf(stderr, "maweb login failed\n"); +				data->login = 0; +				return 0; +		} +		fprintf(stderr, "maweb session id is now %" PRId64 "\n", data->session); +	} + +	if(json_obj_bool(payload, "forceLogin", 0)){ +		fprintf(stderr, "maweb sending user credentials\n"); +		snprintf(xmit_buffer, sizeof(xmit_buffer), +				"{\"requestType\":\"login\",\"username\":\"%s\",\"password\":\"%s\",\"session\":%" PRIu64 "}", +				(data->peer_type == peer_dot2) ? "remote" : data->user, data->pass ? data->pass : MAWEB_DEFAULT_PASSWORD, data->session); +		maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); +	} +	if(json_obj(payload, "status") && json_obj(payload, "appType")){ +		fprintf(stderr, "maweb connection established\n"); +		field = json_obj_str(payload, "appType", NULL); +		if(!strncmp(field, "dot2", 4)){ +			data->peer_type = peer_dot2; +			//the dot2 can't handle lua commands +			data->cmdline = cmd_remote; +		} +		else if(!strncmp(field, "gma2", 4)){ +			data->peer_type = peer_ma2; +		} +		maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); +	} + +	return 0; +} + +static int maweb_connect(instance* inst){ +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; +	if(!data->host){ +		return 1; +	} + +	//unregister old fd from core +	if(data->fd >= 0){ +		mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); +	} + +	data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0); +	if(data->fd < 0){ +		return 1; +	} + +	data->state = ws_new; +	if(mmbackend_send_str(data->fd, "GET /?ma=1 HTTP/1.1\r\n") +			|| mmbackend_send_str(data->fd, "Connection: Upgrade\r\n") +			|| mmbackend_send_str(data->fd, "Upgrade: websocket\r\n") +			|| mmbackend_send_str(data->fd, "Sec-WebSocket-Version: 13\r\n") +			//the websocket key probably should not be hardcoded, but this is not security critical +			//and the whole websocket 'accept key' dance is plenty stupid as it is +			|| mmbackend_send_str(data->fd, "Sec-WebSocket-Key: rbEQrXMEvCm4ZUjkj6juBQ==\r\n") +			|| mmbackend_send_str(data->fd, "\r\n")){ +		fprintf(stderr, "maweb backend failed to communicate with peer\n"); +		return 1; +	} + +	//register new fd +	if(mm_manage_fd(data->fd, BACKEND_NAME, 1, (void*) inst)){ +		fprintf(stderr, "maweb backend failed to register fd\n"); +		return 1; +	} +	return 0; +} + +static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; +	size_t n, begin = 0; + +	for(n = 0; n < bytes_read - 1; n++){ +		if(!strncmp((char*) data->buffer + data->offset + n, "\r\n", 2)){ +			if(data->state == ws_new){ +				if(!strncmp((char*) data->buffer, "HTTP/1.1 101", 12)){ +					data->state = ws_http; +				} +				else{ +					fprintf(stderr, "maweb received invalid HTTP response for instance %s\n", inst->name); +					return -1; +				} +			} +			else{ +				//ignore all http stuff until the end of headers since we don't actually care... +				if(n == begin){ +					data->state = ws_open; +				} +			} +			begin = n + 2; +		} +	} + +	return data->offset + begin; +} + +static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; +	size_t header_length = 2; +	uint64_t payload_length = 0; +	uint16_t* payload_len16 = (uint16_t*) (data->buffer + 2); +	uint64_t* payload_len64 = (uint64_t*) (data->buffer + 2); +	uint8_t* payload = data->buffer + 2; +	uint8_t terminator_temp = 0; + +	if(data->offset + bytes_read < 2){ +		return 0; +	} + +	//using varint as payload length is stupid, but some people seem to think otherwise... +	payload_length = WS_LEN(data->buffer[1]); +	switch(payload_length){ +		case 126: +			if(data->offset + bytes_read < 4){ +				return 0; +			} +			payload_length = htobe16(*payload_len16); +			payload = data->buffer + 4; +			header_length = 4; +			break; +		case 127: +			if(data->offset + bytes_read < 10){ +				return 0; +			} +			payload_length = htobe64(*payload_len64); +			payload = data->buffer + 10; +			header_length = 10; +			break; +		default: +			break; +	} + +	if(data->offset + bytes_read < header_length + payload_length){ +		return 0; +	} + +	switch(WS_OP(data->buffer[0])){ +		case ws_text: +			//terminate message +			terminator_temp = payload[payload_length]; +			payload[payload_length] = 0; +			if(maweb_handle_message(inst, (char*) payload, payload_length)){ +				return data->offset + bytes_read; +			} +			payload[payload_length] = terminator_temp; +			break; +		case ws_ping: +			//answer server ping with a pong +			if(maweb_send_frame(inst, ws_pong, payload, payload_length)){ +				fprintf(stderr, "maweb failed to send pong\n"); +			} +			return header_length + payload_length; +		default: +			fprintf(stderr, "maweb encountered unhandled frame type %02X\n", WS_OP(data->buffer[0])); +			//this is somewhat dicey, it might be better to handle only header + payload length for known but unhandled types +			return data->offset + bytes_read; +	} + +	return header_length + payload_length; +} + +static int maweb_handle_fd(instance* inst){ +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; +	ssize_t bytes_read, bytes_left = data->allocated - data->offset, bytes_handled; + +	if(bytes_left < 3){ +		data->buffer = realloc(data->buffer, (data->allocated + MAWEB_RECV_CHUNK) * sizeof(uint8_t)); +		if(!data->buffer){ +			fprintf(stderr, "Failed to allocate memory\n"); +			return 1; +		} +		data->allocated += MAWEB_RECV_CHUNK; +		bytes_left += MAWEB_RECV_CHUNK; +	} + +	bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0); +	if(bytes_read < 0){ +		fprintf(stderr, "maweb backend failed to receive: %s\n", strerror(errno)); +		//TODO close, reopen +		return 1; +	} +	else if(bytes_read == 0){ +		//client closed connection +		//TODO try to reopen +		return 0; +	} + +	do{ +		switch(data->state){ +			case ws_new: +			case ws_http: +				bytes_handled = maweb_handle_lines(inst, bytes_read); +				break; +			case ws_open: +				bytes_handled = maweb_handle_ws(inst, bytes_read); +				break; +			case ws_closed: +				bytes_handled = data->offset + bytes_read; +				break; +		} + +		if(bytes_handled < 0){ +			bytes_handled = data->offset + bytes_read; +			data->offset = 0; +			//TODO close, reopen +			fprintf(stderr, "maweb failed to handle incoming data\n"); +			return 1; +		} +		else if(bytes_handled == 0){ +			 break; +		} + +		memmove(data->buffer, data->buffer + bytes_handled, (data->offset + bytes_read) - bytes_handled); + +		bytes_handled -= data->offset; +		bytes_read -= bytes_handled; +		data->offset = 0; +	} while(bytes_read > 0); + +	data->offset += bytes_read; +	return 0; +} + +static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ +	maweb_instance_data* data = (maweb_instance_data*) inst->impl; +	maweb_channel_data* chan = NULL; +	char xmit_buffer[MAWEB_XMIT_CHUNK]; +	size_t n; + +	if(num && !data->login){ +		fprintf(stderr, "maweb instance %s can not send output, not logged in\n", inst->name); +		return 0; +	} + +	for(n = 0; n < num; n++){ +		//sanity check +		if(c[n]->ident >= data->channels){ +			return 1; +		} +		chan = data->channel + c[n]->ident; + +		//channel state tracking +		if(chan->out == v[n].normalised){ +			continue; +		} +		chan->out = v[n].normalised; + +		//i/o value space separation & feedback filtering for faders +		if(chan->type == exec_fader){ +			chan->input_blocked = 1; +			chan->in = v[n].normalised; +		} + +		switch(chan->type){ +			case exec_fader: +				snprintf(xmit_buffer, sizeof(xmit_buffer), +						"{\"requestType\":\"playbacks_userInput\"," +						"\"execIndex\":%d," +						"\"pageIndex\":%d," +						"\"faderValue\":%f," +						"\"type\":1," +						"\"session\":%" PRIu64 +						"}", chan->index, chan->page, v[n].normalised, data->session); +				break; +			case exec_upper: +			case exec_lower: +			case exec_button: +				snprintf(xmit_buffer, sizeof(xmit_buffer), +						"{\"requestType\":\"playbacks_userInput\"," +						//"\"cmdline\":\"\"," +						"\"execIndex\":%d," +						"\"pageIndex\":%d," +						"\"buttonId\":%d," +						"\"pressed\":%s," +						"\"released\":%s," +						"\"type\":0," +						"\"session\":%" PRIu64 +						"}", chan->index, chan->page, +						(data->peer_type == peer_dot2 && chan->type == exec_upper) ? 0 : (chan->type - exec_button), +						(v[n].normalised > 0.9) ? "true" : "false", +						(v[n].normalised > 0.9) ? "false" : "true", +						data->session); +				break; +			case cmdline: +				if(cmdline_keys[chan->index].lua +						&& (data->cmdline == cmd_console || data->cmdline == cmd_downgrade) +						&& data->peer_type != peer_dot2){ +					//push canbus events +					snprintf(xmit_buffer, sizeof(xmit_buffer), +							"{\"command\":\"LUA 'gma.canbus.hardkey(%d, %s, false)'\"," +							"\"requestType\":\"command\"," +							"\"session\":%" PRIu64 +							"}", cmdline_keys[chan->index].lua, +							(v[n].normalised > 0.9) ? "true" : "false", +							data->session); +				} +				else if((cmdline_keys[chan->index].press || cmdline_keys[chan->index].release) +						&& (data->cmdline != cmd_console)){ +					//send press/release events if required +					if((cmdline_keys[chan->index].press && v[n].normalised > 0.9) +							|| (cmdline_keys[chan->index].release && v[n].normalised < 0.9)){ +						snprintf(xmit_buffer, sizeof(xmit_buffer), +								"{\"keyname\":\"%s\"," +								"\"autoSubmit\":%s," +								"\"value\":%d," +								"\"session\":%" PRIu64 +								"}", cmdline_keys[chan->index].name, +								cmdline_keys[chan->index].auto_submit ? "true" : "null", +								(v[n].normalised > 0.9) ? 1 : 0, +								data->session); +					} +					else{ +						continue; +					} +				} +				else{ +					fprintf(stderr, "maweb commandline key %s not executed on %s due to mode mismatch\n", +							cmdline_keys[chan->index].name, inst->name); +					continue; +				} +				break; +			default: +				fprintf(stderr, "maweb control not yet implemented\n"); +				return 1; +		} +		DBGPF("maweb command out %s\n", xmit_buffer); +		maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); +	} +	return 0; +} + +static int maweb_keepalive(){ +	size_t n, u; +	instance** inst = NULL; +	maweb_instance_data* data = NULL; +	char xmit_buffer[MAWEB_XMIT_CHUNK]; + +	//fetch all defined instances +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		return 1; +	} + +	//send keep-alive messages for logged-in instances +	for(u = 0; u < n; u++){ +		data = (maweb_instance_data*) inst[u]->impl; +		if(data->login){ +			snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"session\":%" PRIu64 "}", data->session); +			maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); +		} +	} + +	free(inst); +	return 0; +} + +static int maweb_poll(){ +	size_t n, u; +	instance** inst = NULL; +	maweb_instance_data* data = NULL; + +	//fetch all defined instances +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		return 1; +	} + +	//send data polls for logged-in instances +	for(u = 0; u < n; u++){ +		data = (maweb_instance_data*) inst[u]->impl; +		if(data->login){ +			maweb_request_playbacks(inst[u]); +		} +	} + +	free(inst); +	return 0; +} + +static int maweb_handle(size_t num, managed_fd* fds){ +	size_t n = 0; +	int rv = 0; + +	for(n = 0; n < num; n++){ +		rv |= maweb_handle_fd((instance*) fds[n].impl); +	} + +	//FIXME all keepalive processing allocates temporary buffers, this might an optimization target +	if(last_keepalive && mm_timestamp() - last_keepalive >= MAWEB_CONNECTION_KEEPALIVE){ +		rv |= maweb_keepalive(); +		last_keepalive = mm_timestamp(); +	} + +	if(last_update && mm_timestamp() - last_update >= update_interval){ +		rv |= maweb_poll(); +		last_update = mm_timestamp(); +	} + +	return rv; +} + +static int maweb_start(){ +	size_t n, u, p; +	instance** inst = NULL; +	maweb_instance_data* data = NULL; + +	//fetch all defined instances +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		return 1; +	} + +	for(u = 0; u < n; u++){ +		//sort channels +		data = (maweb_instance_data*) inst[u]->impl; +		qsort(data->channel, data->channels, sizeof(maweb_channel_data), channel_comparator); + +		//re-set channel identifiers +		for(p = 0; p < data->channels; p++){ +			data->channel[p].chan->ident = p; +		} + +		if(maweb_connect(inst[u])){ +			fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); +			free(inst); +			return 1; +		} +	} + +	free(inst); +	if(!n){ +		return 0; +	} + +	fprintf(stderr, "maweb backend registering %" PRIsize_t " descriptors to core\n", n); + +	//initialize timeouts +	last_keepalive = last_update = mm_timestamp(); +	return 0; +} + +static int maweb_shutdown(){ +	size_t n, u; +	instance** inst = NULL; +	maweb_instance_data* data = NULL; + +	//fetch all instances +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		return 1; +	} + +	for(u = 0; u < n; u++){ +		data = (maweb_instance_data*) inst[u]->impl; +		free(data->host); +		data->host = NULL; +		free(data->port); +		data->port = NULL; +		free(data->user); +		data->user = NULL; +		free(data->pass); +		data->pass = NULL; + +		close(data->fd); +		data->fd = -1; + +		free(data->buffer); +		data->buffer = NULL; + +		data->offset = data->allocated = 0; +		data->state = ws_new; + +		free(data->channel); +		data->channel = NULL; +		data->channels = 0; +	} + +	free(inst); + +	fprintf(stderr, "maweb backend shut down\n"); +	return 0; +} diff --git a/backends/maweb.h b/backends/maweb.h new file mode 100644 index 0000000..05095f8 --- /dev/null +++ b/backends/maweb.h @@ -0,0 +1,100 @@ +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static int maweb_configure(char* option, char* value); +static int maweb_configure_instance(instance* inst, char* option, char* value); +static instance* maweb_instance(); +static channel* maweb_channel(instance* inst, char* spec, uint8_t flags); +static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v); +static int maweb_handle(size_t num, managed_fd* fds); +static int maweb_start(); +static int maweb_shutdown(); +static uint32_t maweb_interval(); + +//Default login password: MD5("midimonster") +#define MAWEB_DEFAULT_PASSWORD "2807623134739142b119aff358f8a219" +#define MAWEB_DEFAULT_PORT "80" +#define MAWEB_RECV_CHUNK 1024 +#define MAWEB_XMIT_CHUNK 4096 +#define MAWEB_FRAME_HEADER_LENGTH 16 +#define MAWEB_CONNECTION_KEEPALIVE 10000 + +typedef enum /*_maweb_channel_type*/ { +	type_unset = 0, +	exec_fader = 1, +	exec_button = 2, //gma: 0 dot: 0 +	exec_lower = 3, //gma: 1 dot: 1 +	exec_upper = 4, //gma: 2 dot: 0 +	cmdline +} maweb_channel_type; + +typedef enum /*_maweb_peer_type*/ { +	peer_unidentified = 0, +	peer_ma2, +	peer_ma3, +	peer_dot2 +} maweb_peer_type; + +typedef enum /*_ws_conn_state*/ { +	ws_new, +	ws_http, +	ws_open, +	ws_closed +} maweb_state; + +typedef enum /*_maweb_cmdline_mode*/ { +	cmd_remote = 0, +	cmd_console, +	cmd_downgrade +} maweb_cmdline_mode; + +typedef enum /*_ws_frame_op*/ { +	ws_text = 1, +	ws_binary = 2, +	ws_ping = 9, +	ws_pong = 10 +} maweb_operation; + +typedef struct { +	char* name; +	unsigned lua; +	uint8_t press; +	uint8_t release; +	uint8_t auto_submit; +} maweb_command_key; + +typedef struct /*_maweb_channel*/ { +	maweb_channel_type type; +	uint16_t page; +	uint16_t index; + +	uint8_t input_blocked; + +	double in; +	double out; + +	//reverse reference required because the identifiers are not stable +	//because we sort the backing store... +	channel* chan; +} maweb_channel_data; + +typedef struct /*_maweb_instance_data*/ { +	char* host; +	char* port; +	char* user; +	char* pass; + +	uint8_t login; +	int64_t session; +	maweb_peer_type peer_type; + +	size_t channels; +	maweb_channel_data* channel; +	maweb_cmdline_mode cmdline; + +	int fd; +	maweb_state state; +	size_t offset; +	size_t allocated; +	uint8_t* buffer; +} maweb_instance_data; diff --git a/backends/maweb.md b/backends/maweb.md new file mode 100644 index 0000000..45dc778 --- /dev/null +++ b/backends/maweb.md @@ -0,0 +1,141 @@ +### The `maweb` backend + +This backend connects directly with the integrated *MA Web Remote* of MA Lighting consoles and OnPC +instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC). +It grants read-write access to the console's playback controls as well as write access to most command +line and control keys. + +#### Setting up the console + +For the GrandMA2 enter the console configuration (`Setup` key), select `Console`/`Global Settings` and +set the `Remotes` option to `Login enabled`. +Create an additional user that is able to log into the Web Remote using `Setup`/`Console`/`User & Profiles Setup`. + +For the dot2, enter the console configuration using the `Setup` key, select `Global Settings` and enable the +Web Remote. Set a web remote password using the option below the activation setting. + +#### Global configuration + +| Option	| Example value		| Default value		| Description							| +|---------------|-----------------------|-----------------------|---------------------------------------------------------------| +| `interval`	| `100`			| `50`			| Query interval for input data polling (in msec)		| + +#### Instance configuration + +| Option	| Example value		| Default value		| Description							| +|---------------|-----------------------|-----------------------|---------------------------------------------------------------| +| `host`	| `10.23.42.21 80`	| none			| Host address (and optional port) of the MA Web Remote		| +| `user`	| `midimonster`		| none			| User for the remote session (GrandMA2)			| +| `password`	| `midimonster`		| `midimonster`		| Password for the remote session				| +| `cmdline`	| `console`		| `remote`		| Commandline key handling mode (see below)			| + +The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles +command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are +not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects key events +into the main console. This mode also supports additional hardkeys that are only available on GrandMA consoles. +When connected to a dot2 console while this mode is active, the use of commandline keys will not be possible. +With the `downgrade` mode, keys are handled on the console if possible, falling back to remote handling if not. + +#### Channel specification + +Currently, three types of MA controls can be assigned, with each having some subcontrols + +* Fader executor +* Button executor +* Command keys + +##### Executors + +* For the GrandMA2, executors are arranged in pages, with each page having 90 fader executors (numbered 1 through 90) +	and 90 button executors (numbered 101 through 190). +	* A fader executor consists of a `fader`, two buttons above it (`upper`, `lower`) and one `button` below it. +	* A button executor consists of a `button` control and a virtual `fader` (visible on the console in the "Action Buttons" view). +* For the dot2, executors are also arranged in pages, but the controls are non-obviously numbered. +	* For the faders, they are numerically right-to-left from the Core Fader section (Faders 6 to 1) over the F-Wing 1 (Faders 13 to 6) to +	F-Wing 2 (Faders 21 to 14). +	* Above the fader sections are two rows of 21 `button` executors, numbered 122 through 101 (lower row) and 222 through 201 (upper row), +		in the same order as the faders are. +	* Fader executors have two buttons below them (`upper` and `lower`). +	* The button executor section consists of six rows of 16 buttons, divided into two button wings. Buttons on the wings +		are once again numbered right-to-left. +		* B-Wing 1 has `button` controls 308 to 301 (top row), 408 to 401 (second row), and so on until 808 through 801 (bottom row) +		* B-Wing 2 has 316 to 309 (top row) through 816 to 809 (bottom row) + +When creating a new show, only the first page is created and active. Additional pages have to be created explicitly within +the console before being usable. When mapped as outputs, `fader` controls output their value, `button` controls output 1 when the corresponding +executor is running, 0 otherwise. + +These controls can be addressed like + +``` +mw1.page1.fader5 > mw1.page1.upper5 +mw1.page3.lower3 > mw1.page2.button2 +``` + +A button executor can likewise be mapped using the syntax + +``` +mw1.page2.button103 > mw1.page3.fader101 +mw1.page2.button803 > mw1.page3.button516 +``` + +##### Command keys + +Command keys will be pressed when the incoming event value is greater than `0.9` and released when it is less than that. +They can be mapped using the syntax + +``` +mw1.<key-name> +``` + +The following keys are mappable in all commandline modes and work on all consoles + +| Supported	| Command	| Line		| Keys		|		|		| +|---------------|---------------|---------------|---------------|---------------|---------------| +| `PREV`	| `SET`		| `NEXT`	| `TIME`	| `EDIT`	| `UPDATE`	| +| `OOPS`	| `ESC`		| `CLEAR`	| `0`		| `1`		| `2`		| +| `3`		| `4`		| `5`		| `6`		| `7`		| `8`		| +| `9`		| `PUNKT`	| `ENTER`	| `PLUS`	| `MINUS`	| `THRU`	| +| `IF`		| `AT`		| `FULL`	| `MA`		| `HIGH`	| `SOLO`	| +| `SELECT`	| `OFF`		| `ON`		| `ASSIGN`	| `COPY`	| `DELETE`	| +| `STORE`	| `GOTO`	| `PAGE`	| `MACRO`	| `PRESET`	| `SEQU`	| +| `CUE`		| `EXEC`	| `FIXTURE`	| `GROUP`	| `GO_MINUS`	| `PAUSE`	| +| `GO_PLUS`	|		|		|		|		|		| + +The following keys only work when keys are being handled with a virtual command line + +| Web		| Remote		| specific			|		|			| +|---------------|-----------------------|-------------------------------|---------------|-----------------------| +| `LABEL`	|`FIXTURE_CHANNEL`	| `FIXTURE_GROUP_PRESET`	| `EXEC_CUE`	| `STORE_UPDATE`	| +| `PROG_ONLY`	| `SPECIAL_DIALOGUE`	| `ODD`				| `EVEN`	| `WINGS`		| +| `RESET`	|			|				|		|			| + +The following keys only work in the `console` or `downgrade` command line modes on a GrandMA2 + +| GrandMA2	| console	| only		|		|		|		| +|---------------|---------------|---------------|---------------|---------------|---------------| +| `CHPGPLUS`	| `CHPGMINUS`	| `FDPGPLUS`	| `FDPGMINUS`	| `BTPGPLUS`	| `BTPGMINUS`	| +| `X1`		| `X2`		| `X3`		| `X4`		| `X5`		| `X6`		| +| `X7`		| `X8`		| `X9`		| `X10`		| `X11`		| `X12`		| +| `X13`		| `X14`		| `X15`		| `X16`		| `X17`		| `X18`		| +| `X19`		| `X20`		| `V1`		| `V2`		| `V3`		| `V4`		| +| `V5`		| `V6`		| `V7`		| `V8`		| `V9`		| `V10`		| +| `NIPPLE`	| `TOOLS`	| `SETUP`	| `BACKUP`	| `BLIND`	| `FREEZE`	| +| `PREVIEW`	| `FIX`		| `TEMP`	| `TOP`		| `VIEW`	| `EFFECT`	| +| `CHANNEL`	| `MOVE`	| `BLACKOUT`	| `PLEASE`	| `LIST`	| `USER1`	| +| `USER2`	| `ALIGN`	| `HELP`	| `UP`		| `DOWN`	| `FASTREVERSE`	| +| `LEARN`	| `FASTFORWARD`	| `GO_MINUS_SMALL` | `PAUSE_SMALL` | `GO_PLUS_SMALL` |		| + +#### Known bugs / problems + +To properly encode the user password, this backend depends on a library providing cryptographic functions (`libssl` / `openssl`). +Since this may be a problem on some platforms, the backend can be built with this requirement disabled, which also disables the possibility +to set arbitrary passwords. The backend will always try to log in with the default password `midimonster` in this case. The user name is still +configurable. + +Data input from the console is done by actively querying the state of all mapped controls, which is resource-intensive if done +at low latency. A lower input interval value will produce data with lower latency, at the cost of network & CPU usage. +Higher values will make the input "step" more, but will not consume as many CPU cycles and network bandwidth. + +When requesting button executor events on the fader pages (execs 101 to 222) of a dot2 console, map at least one fader control from the 0 - 22 range +or input will not work due to strange limitations in the MA Web API. diff --git a/backends/midi.c b/backends/midi.c index d856ced..92776ca 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -3,32 +3,27 @@  #include "midi.h"  #define BACKEND_NAME "midi" +static char* sequencer_name = NULL;  static snd_seq_t* sequencer = NULL; -typedef union { -	struct { -		uint8_t pad[5]; -		uint8_t type; -		uint8_t channel; -		uint8_t control; -	} fields; -	uint64_t label; -} midi_channel_ident; - -/* - * TODO - * 	Optionally send note-off messages - * 	Optionally send updates as after-touch - */  enum /*_midi_channel_type*/ {  	none = 0,  	note,  	cc, +	pressure, +	aftertouch, +	pitchbend,  	nrpn,  	sysmsg  }; -int init(){ +static struct { +	uint8_t detect; +} midi_config = { +	.detect = 0 +}; + +MM_PLUGIN_API int init(){  	backend midi = {  		.name = BACKEND_NAME,  		.conf = midi_configure, @@ -41,8 +36,8 @@ int init(){  		.shutdown = midi_shutdown  	}; -	if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){ -		fprintf(stderr, "Failed to open ALSA sequencer\n"); +	if(sizeof(midi_channel_ident) != sizeof(uint64_t)){ +		fprintf(stderr, "MIDI channel identification union out of bounds\n");  		return 1;  	} @@ -52,17 +47,19 @@ int init(){  		return 1;  	} -	snd_seq_nonblock(sequencer, 1);		 -		 -	fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer));  	return 0;  }  static int midi_configure(char* option, char* value){  	if(!strcmp(option, "name")){ -		if(snd_seq_set_client_name(sequencer, value) < 0){ -			fprintf(stderr, "Failed to set MIDI client name to %s\n", value); -			return 1; +		free(sequencer_name); +		sequencer_name = strdup(value); +		return 0; +	} +	else if(!strcmp(option, "detect")){ +		midi_config.detect = 1; +		if(!strcmp(value, "off")){ +			midi_config.detect = 0;  		}  		return 0;  	} @@ -86,14 +83,14 @@ static instance* midi_instance(){  	return inst;  } -static int midi_configure_instance(instance* instance, char* option, char* value){ -	midi_instance_data* data = (midi_instance_data*) instance->impl; +static int midi_configure_instance(instance* inst, char* option, char* value){ +	midi_instance_data* data = (midi_instance_data*) inst->impl;  	//FIXME maybe allow connecting more than one device  	if(!strcmp(option, "read")){  		//connect input device  		if(data->read){ -			fprintf(stderr, "MIDI port already connected to an input device\n"); +			fprintf(stderr, "MIDI instance %s was already connected to an input device\n", inst->name);  			return 1;  		}  		data->read = strdup(value); @@ -102,7 +99,7 @@ static int midi_configure_instance(instance* instance, char* option, char* value  	else if(!strcmp(option, "write")){  		//connect output device  		if(data->write){ -			fprintf(stderr, "MIDI port already connected to an output device\n"); +			fprintf(stderr, "MIDI instance %s was already connected to an output device\n", inst->name);  			return 1;  		}  		data->write = strdup(value); @@ -113,48 +110,87 @@ static int midi_configure_instance(instance* instance, char* option, char* value  	return 1;  } -static channel* midi_channel(instance* instance, char* spec){ +static channel* midi_channel(instance* inst, char* spec, uint8_t flags){  	midi_channel_ident ident = {  		.label = 0  	}; +	//support deprecated syntax for a transition period... +	uint8_t old_syntax = 0;  	char* channel; -	if(!strncmp(spec, "cc", 2)){ +	if(!strncmp(spec, "ch", 2)){ +		channel = spec + 2; +		if(!strncmp(spec, "channel", 7)){ +			channel = spec + 7; +		} +	} +	else if(!strncmp(spec, "cc", 2)){  		ident.fields.type = cc;  		channel = spec + 2; +		old_syntax = 1;  	}  	else if(!strncmp(spec, "note", 4)){  		ident.fields.type = note;  		channel = spec + 4; +		old_syntax = 1;  	}  	else if(!strncmp(spec, "nrpn", 4)){  		ident.fields.type = nrpn;  		channel = spec + 4; +		old_syntax = 1;  	}  	else{ -		fprintf(stderr, "Unknown MIDI channel specification %s\n", spec); +		fprintf(stderr, "Unknown MIDI channel control type in %s\n", spec);  		return NULL;  	}  	ident.fields.channel = strtoul(channel, &channel, 10); - -	//FIXME test this -	if(ident.fields.channel > 16){ -		fprintf(stderr, "MIDI channel out of range in channel spec %s\n", spec); +	if(ident.fields.channel > 15){ +		fprintf(stderr, "MIDI channel out of range in midi channel spec %s\n", spec);  		return NULL;  	}  	if(*channel != '.'){ -		fprintf(stderr, "Need MIDI channel specification of form channel.control, had %s\n", spec); +		fprintf(stderr, "Need MIDI channel specification of form channel<X>.<control><Y>, had %s\n", spec);  		return NULL;  	} +	//skip the period  	channel++; +	if(!old_syntax){ +		if(!strncmp(channel, "cc", 2)){ +			ident.fields.type = cc; +			channel += 2; +		} +		else if(!strncmp(channel, "note", 4)){ +			ident.fields.type = note; +			channel += 4; +		} +		else if(!strncmp(channel, "nrpn", 4)){ +			ident.fields.type = nrpn; +			channel += 4; +		} +		else if(!strncmp(channel, "pressure", 8)){ +			ident.fields.type = pressure; +			channel += 8; +		} +		else if(!strncmp(channel, "pitch", 5)){ +			ident.fields.type = pitchbend; +		} +		else if(!strncmp(channel, "aftertouch", 10)){ +			ident.fields.type = aftertouch; +		} +		else{ +			fprintf(stderr, "Unknown MIDI channel control type in %s\n", spec); +			return NULL; +		} +	} +  	ident.fields.control = strtoul(channel, NULL, 10);  	if(ident.label){ -		return mm_channel(instance, ident.label, 1); +		return mm_channel(inst, ident.label, 1);  	}  	return NULL; @@ -163,20 +199,19 @@ static channel* midi_channel(instance* instance, char* spec){  static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){  	size_t u;  	snd_seq_event_t ev; -	midi_instance_data* data; +	midi_instance_data* data = (midi_instance_data*) inst->impl;  	midi_channel_ident ident = {  		.label = 0  	};  	for(u = 0; u < num; u++){ -		data = (midi_instance_data*) c[u]->instance->impl;  		ident.label = c[u]->ident;  		snd_seq_ev_clear(&ev);  		snd_seq_ev_set_source(&ev, data->port);  		snd_seq_ev_set_subs(&ev);  		snd_seq_ev_set_direct(&ev); -		 +  		switch(ident.fields.type){  			case note:  				snd_seq_ev_set_noteon(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); @@ -184,6 +219,15 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){  			case cc:  				snd_seq_ev_set_controller(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0);  				break; +			case pressure: +				snd_seq_ev_set_keypress(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); +				break; +			case pitchbend: +				snd_seq_ev_set_pitchbend(&ev, ident.fields.channel, (v[u].normalised * 16383.0) - 8192); +				break; +			case aftertouch: +				snd_seq_ev_set_chanpress(&ev, ident.fields.channel, v[u].normalised * 127.0); +				break;  			case nrpn:  				//FIXME set to nrpn output  				break; @@ -201,6 +245,7 @@ static int midi_handle(size_t num, managed_fd* fds){  	instance* inst = NULL;  	channel* changed = NULL;  	channel_value val; +	char* event_type = NULL;  	midi_channel_ident ident = {  		.label = 0  	}; @@ -210,16 +255,39 @@ static int midi_handle(size_t num, managed_fd* fds){  	}  	while(snd_seq_event_input(sequencer, &ev) > 0){ +		event_type = NULL;  		ident.label = 0;  		switch(ev->type){  			case SND_SEQ_EVENT_NOTEON:  			case SND_SEQ_EVENT_NOTEOFF: -			case SND_SEQ_EVENT_KEYPRESS:  			case SND_SEQ_EVENT_NOTE:  				ident.fields.type = note;  				ident.fields.channel = ev->data.note.channel;  				ident.fields.control = ev->data.note.note;  				val.normalised = (double)ev->data.note.velocity / 127.0; +				if(ev->type == SND_SEQ_EVENT_NOTEOFF){ +   					val.normalised = 0; +				} +				event_type = "note"; +				break; +			case SND_SEQ_EVENT_KEYPRESS: +				ident.fields.type = pressure; +				ident.fields.channel = ev->data.note.channel; +				ident.fields.control = ev->data.note.note; +				val.normalised = (double)ev->data.note.velocity / 127.0; +				event_type = "pressure"; +				break; +			case SND_SEQ_EVENT_CHANPRESS: +				ident.fields.type = aftertouch; +				ident.fields.channel = ev->data.control.channel; +				val.normalised = (double)ev->data.control.value / 127.0; +				event_type = "aftertouch"; +				break; +			case SND_SEQ_EVENT_PITCHBEND: +				ident.fields.type = pitchbend; +				ident.fields.channel = ev->data.control.channel; +				val.normalised = ((double)ev->data.control.value + 8192) / 16383.0; +				event_type = "pitch";  				break;  			case SND_SEQ_EVENT_CONTROLLER:  				ident.fields.type = cc; @@ -227,6 +295,7 @@ static int midi_handle(size_t num, managed_fd* fds){  				ident.fields.control = ev->data.control.param;  				val.raw.u64 = ev->data.control.value;  				val.normalised = (double)ev->data.control.value / 127.0; +				event_type = "cc";  				break;  			case SND_SEQ_EVENT_CONTROL14:  			case SND_SEQ_EVENT_NONREGPARAM: @@ -255,13 +324,22 @@ static int midi_handle(size_t num, managed_fd* fds){  				return 1;  			}  		} + +		if(midi_config.detect && event_type){ +			if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){ +				fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s\n", inst->name, ident.fields.channel, event_type); +			} +			else{ +				fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d\n", inst->name, ident.fields.channel, event_type, ident.fields.control); +			} +		}  	}  	free(ev);  	return 0;  }  static int midi_start(){ -	size_t n, p; +	size_t n = 0, p;  	int nfds, rv = 1;  	struct pollfd* pfds = NULL;  	instance** inst = NULL; @@ -279,6 +357,21 @@ static int midi_start(){  		return 0;  	} +	//connect to the sequencer +	if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){ +		fprintf(stderr, "Failed to open ALSA sequencer\n"); +		goto bail; +	} + +	snd_seq_nonblock(sequencer, 1); +	fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer)); + +	//update the sequencer client name +	if(snd_seq_set_client_name(sequencer, sequencer_name ? sequencer_name : "MIDIMonster") < 0){ +		fprintf(stderr, "Failed to set MIDI client name to %s\n", sequencer_name); +		goto bail; +	} +  	//create all ports  	for(p = 0; p < n; p++){  		data = (midi_instance_data*) inst[p]->impl; @@ -312,13 +405,13 @@ static int midi_start(){  	}  	//register all fds to core -	nfds = snd_seq_poll_descriptors_count(sequencer, POLLIN | POLLOUT);	 +	nfds = snd_seq_poll_descriptors_count(sequencer, POLLIN | POLLOUT);  	pfds = calloc(nfds, sizeof(struct pollfd));  	if(!pfds){  		fprintf(stderr, "Failed to allocate memory\n");  		goto bail;  	} -	nfds = snd_seq_poll_descriptors(sequencer, pfds, nfds, POLLIN | POLLOUT);	 +	nfds = snd_seq_poll_descriptors(sequencer, pfds, nfds, POLLIN | POLLOUT);  	fprintf(stderr, "MIDI backend registering %d descriptors to core\n", nfds);  	for(p = 0; p < nfds; p++){ @@ -355,12 +448,17 @@ static int midi_shutdown(){  	free(inst);  	//close midi -	snd_seq_close(sequencer); -	sequencer = NULL; +	if(sequencer){ +		snd_seq_close(sequencer); +		sequencer = NULL; +	}  	//free configuration cache  	snd_config_update_free_global(); +	free(sequencer_name); +	sequencer_name = NULL; +  	fprintf(stderr, "MIDI backend shut down\n");  	return 0;  } diff --git a/backends/midi.h b/backends/midi.h index 556706f..4e16f90 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -1,10 +1,10 @@  #include "midimonster.h" -int init(); +MM_PLUGIN_API int init();  static int midi_configure(char* option, char* value);  static int midi_configure_instance(instance* instance, char* option, char* value);  static instance* midi_instance(); -static channel* midi_channel(instance* instance, char* spec); +static channel* midi_channel(instance* instance, char* spec, uint8_t flags);  static int midi_set(instance* inst, size_t num, channel** c, channel_value* v);  static int midi_handle(size_t num, managed_fd* fds);  static int midi_start(); @@ -15,3 +15,14 @@ typedef struct /*_midi_instance_data*/ {  	char* read;  	char* write;  } midi_instance_data; + +typedef union { +	struct { +		uint8_t pad[5]; +		uint8_t type; +		uint8_t channel; +		uint8_t control; +	} fields; +	uint64_t label; +} midi_channel_ident; + diff --git a/backends/midi.md b/backends/midi.md new file mode 100644 index 0000000..108860e --- /dev/null +++ b/backends/midi.md @@ -0,0 +1,65 @@ +### The `midi` backend + +The MIDI backend provides read-write access to the MIDI protocol via virtual ports. + +#### Global configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `name`	| `MIDIMonster`		| none			| MIDI client name	| +| `detect`      | `on`                  | `off`                 | Output channel specifications for any events coming in on configured instances to help with configuration. | + +#### Instance configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `read`	| `20:0`		| none			| MIDI device to connect for input | +| `write`	| `DeviceName`		| none			| MIDI device to connect for output | + +MIDI device names may either be `client:port` portnames or prefixes of MIDI device names. +Run `aconnect -i` to list input ports and `aconnect -o` to list output ports. + +Each instance also provides a virtual port, so MIDI devices can also be connected with `aconnect <sender> <receiver>`. + +#### Channel specification + +The MIDI 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 +* `nrpn` - NRPNs (not yet implemented) + +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 earlier syntax of `<type><channel>.<index>` is officially deprecated but still supported for compatibility +reasons. This support may be removed at some future time. + +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: +``` +midi1.ch0.note9 > midi2.channel1.cc4 +midi1.channel15.pressure1 > midi1.channel0.note0 +midi1.ch1.aftertouch > midi2.ch2.cc0 +midi1.ch0.pitch > midi2.ch1.pitch +``` +#### Known bugs / problems + +To access MIDI data, the user running MIDIMonster needs read & write access to the ALSA sequencer. +This can usually be done by adding this user to the `audio` system group. + +Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are +generated, which amount to the same thing according to the spec). This may be implemented as +a configuration option at a later time. + +NRPNs are not yet fully implemented, though rudimentary support is in the codebase. + +To see which events your MIDI devices output, ALSA provides the `aseqdump` utility. You can +list all incoming events using `aseqdump -p <portname>`. diff --git a/backends/ola.cpp b/backends/ola.cpp new file mode 100644 index 0000000..c13e8f9 --- /dev/null +++ b/backends/ola.cpp @@ -0,0 +1,318 @@ +#include "ola.h" +#include <cstring> +#include <ola/DmxBuffer.h> +#include <ola/Logging.h> +#include <ola/OlaClientWrapper.h> +#include <ola/client/OlaClient.h> +#include <ola/io/SelectServer.h> +#include <ola/network/Socket.h> + +#define BACKEND_NAME "ola" +static ola::io::SelectServer* ola_select = NULL; +static ola::OlaCallbackClient* ola_client = NULL; + +MM_PLUGIN_API int init(){ +	backend ola = { +		.name = BACKEND_NAME, +		.conf = ola_configure, +		.create = ola_instance, +		.conf_instance = ola_configure_instance, +		.channel = ola_channel, +		.handle = ola_set, +		.process = ola_handle, +		.start = ola_start, +		.shutdown = ola_shutdown +	}; + +	//register backend +	if(mm_backend_register(ola)){ +		fprintf(stderr, "Failed to register OLA backend\n"); +		return 1; +	} + +	ola::InitLogging(ola::OLA_LOG_WARN, ola::OLA_LOG_STDERR); +	return 0; +} + +static int ola_configure(char* option, char* value){ +	fprintf(stderr, "Unknown OLA backend option %s\n", option); +	return 1; +} + +static instance* ola_instance(){ +	ola_instance_data* data = NULL; +	instance* inst = mm_instance(); +	if(!inst){ +		return NULL; +	} + +	data = (ola_instance_data*)calloc(1, sizeof(ola_instance_data)); +	if(!data){ +		fprintf(stderr, "Failed to allocate memory\n"); +		return NULL; +	} + +	inst->impl = data; +	return inst; +} + +static int ola_configure_instance(instance* inst, char* option, char* value){ +	ola_instance_data* data = (ola_instance_data*) inst->impl; + +	if(!strcmp(option, "universe")){ +		data->universe_id = strtoul(value, NULL, 0); +		return 0; +	} + +	fprintf(stderr, "Unknown OLA option %s for instance %s\n", option, inst->name); +	return 1; +} + +static channel* ola_channel(instance* inst, char* spec, uint8_t flags){ +	ola_instance_data* data = (ola_instance_data*) inst->impl; +	char* spec_next = spec; +	unsigned chan_a = strtoul(spec, &spec_next, 10); +	unsigned chan_b = 0; + +	//primary channel sanity check +	if(!chan_a || chan_a > 512){ +		fprintf(stderr, "Invalid OLA channel specification %s\n", spec); +		return NULL; +	} +	chan_a--; + +	//secondary channel setup +	if(*spec_next == '+'){ +		chan_b = strtoul(spec_next + 1, NULL, 10); +		if(!chan_b || chan_b > 512){ +			fprintf(stderr, "Invalid wide-channel spec %s\n", spec); +			return NULL; +		} +		chan_b--; + +		//if mapped mode differs, bail +		if(IS_ACTIVE(data->data.map[chan_b]) && data->data.map[chan_b] != (MAP_FINE | chan_a)){ +			fprintf(stderr, "Fine channel already mapped for OLA spec %s\n", spec); +			return NULL; +		} + +		data->data.map[chan_b] = MAP_FINE | chan_a; +	} + +	//check current map mode +	if(IS_ACTIVE(data->data.map[chan_a])){ +		if((*spec_next == '+' && data->data.map[chan_a] != (MAP_COARSE | chan_b)) +				|| (*spec_next != '+' && data->data.map[chan_a] != (MAP_SINGLE | chan_a))){ +			fprintf(stderr, "Primary OLA channel already mapped at differing mode: %s\n", spec); +			return NULL; +		} +	} +	data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a); + +	return mm_channel(inst, chan_a, 1); +} + +static int ola_set(instance* inst, size_t num, channel** c, channel_value* v){ +	size_t u, mark = 0; +	ola_instance_data* data = (ola_instance_data*) inst->impl; + +	for(u = 0; u < num; u++){ +		if(IS_WIDE(data->data.map[c[u]->ident])){ +			uint32_t val = v[u].normalised * ((double) 0xFFFF); +			//the primary (coarse) channel is the one registered to the core, so we don't have to check for that +			if(data->data.data[c[u]->ident] != ((val >> 8) & 0xFF)){ +				mark = 1; +				data->data.data[c[u]->ident] = (val >> 8) & 0xFF; +			} + +			if(data->data.data[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){ +				mark = 1; +				data->data.data[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF; +			} +		} +		else if(data->data.data[c[u]->ident] != (v[u].normalised * 255.0)){ +			mark = 1; +			data->data.data[c[u]->ident] = v[u].normalised * 255.0; +		} +	} + +	if(mark){ +		ola_client->SendDmx(data->universe_id, ola::DmxBuffer(data->data.data, 512)); +	} + +	return 0; +} + +static int ola_handle(size_t num, managed_fd* fds){ +	if(!num){ +		return 0; +	} + +	//defer input to ola via the scenic route... +	ola_select->RunOnce(); +	return 0; +} + +static void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dmx, const std::string& error) { +	size_t p, max_mark = 0; +	//this should really be size_t but ola is weird... +	unsigned int dmx_length = 512; +	uint8_t raw_dmx[dmx_length]; +	uint16_t wide_val; +	channel* chan = NULL; +	channel_value val; +	instance* inst = mm_instance_find(BACKEND_NAME, universe); +	if(!inst){ +		return; +	} +	ola_instance_data* data = (ola_instance_data*) inst->impl; +	ola_dmx.Get((uint8_t*)raw_dmx, &dmx_length); + +	//read data into instance universe, mark changed channels +	for(p = 0; p < dmx_length; p++){ +		if(IS_ACTIVE(data->data.map[p]) && raw_dmx[p] != data->data.data[p]){ +			data->data.data[p] = raw_dmx[p]; +			data->data.map[p] |= MAP_MARK; +			max_mark = p; +		} +	} + +	//generate channel events +	for(p = 0; p <= max_mark; p++){ +		if(data->data.map[p] & MAP_MARK){ +			data->data.map[p] &= ~MAP_MARK; +			if(data->data.map[p] & MAP_FINE){ +				chan = mm_channel(inst, MAPPED_CHANNEL(data->data.map[p]), 0); +			} +			else{ +				chan = mm_channel(inst, p, 0); +			} +			 +			if(!chan){ +				fprintf(stderr, "Active channel %zu on %s not known to core\n", p, inst->name); +				return; +			} + +			if(IS_WIDE(data->data.map[p])){ +				data->data.map[MAPPED_CHANNEL(data->data.map[p])] &= ~MAP_MARK; +				wide_val = data->data.data[p] << ((data->data.map[p] & MAP_COARSE) ? 8 : 0); +				wide_val |= data->data.data[MAPPED_CHANNEL(data->data.map[p])] << ((data->data.map[p] & MAP_COARSE) ? 0 : 8); + +				val.raw.u64 = wide_val; +				val.normalised = (double) wide_val / (double) 0xFFFF; +			} +			else{ +				val.raw.u64 = data->data.data[p]; +				val.normalised = (double) data->data.data[p] / 255.0; +			} + +			if(mm_channel_event(chan, val)){ +				fprintf(stderr, "Failed to push OLA channel event to core\n"); +				return; +			} +		} +	} +} + +static void ola_register_callback(const std::string &error) { +	if(!error.empty()){ +		fprintf(stderr, "OLA backend failed to register for universe: %s\n", error.c_str()); +	} +} + +static int ola_start(){ +	size_t n, u, p; +	instance** inst = NULL; +	ola_instance_data* data = NULL; + +	ola_select = new ola::io::SelectServer(); +	ola::network::IPV4SocketAddress ola_server(ola::network::IPV4Address::Loopback(), ola::OLA_DEFAULT_PORT); +	ola::network::TCPSocket* ola_socket = ola::network::TCPSocket::Connect(ola_server); +	if(!ola_socket){ +		fprintf(stderr, "Failed to connect to OLA server\n"); +		return 1; +	} + +	ola_client = new ola::OlaCallbackClient(ola_socket); + +	if(!ola_client->Setup()){ +		fprintf(stderr, "Failed to start OLA client\n"); +		goto bail; +	} + +	ola_select->AddReadDescriptor(ola_socket); + +	fprintf(stderr, "OLA backend registering connection descriptor to core\n"); +	if(mm_manage_fd(ola_socket->ReadDescriptor(), BACKEND_NAME, 1, NULL)){ +		goto bail; +	} + +	ola_client->SetDmxCallback(ola::NewCallback(&ola_data_receive)); + +	//fetch all defined instances +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		goto bail; +	} + +	//this should not happen anymore (backends without instances are not started anymore) +	if(!n){ +		free(inst); +		return 0; +	} + +	for(u = 0; u < n; u++){ +		data = (ola_instance_data*) inst[u]->impl; +		inst[u]->ident = data->universe_id; + +		//check for duplicate instances (using the same universe) +		for(p = 0; p < u; p++){ +			if(inst[u]->ident == inst[p]->ident){ +				fprintf(stderr, "OLA universe used in multiple instances, use one instance: %s - %s\n", inst[u]->name, inst[p]->name); +				goto bail; +			} +		} +		ola_client->RegisterUniverse(data->universe_id, ola::REGISTER, ola::NewSingleCallback(&ola_register_callback)); +	} + +	//run the ola select implementation to run all commands +	ola_select->RunOnce(); +	free(inst); +	return 0; +bail: +	free(inst); +	delete ola_client; +	ola_client = NULL; +	delete ola_select; +	ola_select = NULL; +	return 1; +} + +static int ola_shutdown(){ +	size_t n, p; +	instance** inst = NULL; +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		return 1; +	} + +	for(p = 0; p < n; p++){ +		free(inst[p]->impl); +	} +	free(inst); + +	if(ola_client){ +		ola_client->Stop(); +		delete ola_client; +		ola_client = NULL; +	} + +	if(ola_select){ +		ola_select->Terminate(); +		delete ola_select; +		ola_select = NULL; +	} + +	fprintf(stderr, "OLA backend shut down\n"); +	return 0; +} diff --git a/backends/ola.h b/backends/ola.h new file mode 100644 index 0000000..0c42bac --- /dev/null +++ b/backends/ola.h @@ -0,0 +1,38 @@ +extern "C" { +	#include "midimonster.h" +	//C++ has it's own implementation of these... +	#undef min +	#undef max + +	MM_PLUGIN_API int init(); +	static int ola_configure(char* option, char* value); +	static int ola_configure_instance(instance* instance, char* option, char* value); +	static instance* ola_instance(); +	static channel* ola_channel(instance* instance, char* spec, uint8_t flags); +	static int ola_set(instance* inst, size_t num, channel** c, channel_value* v); +	static int ola_handle(size_t num, managed_fd* fds); +	static int ola_start(); +	static int ola_shutdown(); +} + +#define MAP_COARSE 0x0200 +#define MAP_FINE 0x0400 +#define MAP_SINGLE 0x0800 +#define MAP_MARK 0x1000 +#define MAPPED_CHANNEL(a) ((a) & 0x01FF) +#define IS_ACTIVE(a) ((a) & 0xFE00) +#define IS_WIDE(a) ((a) & (MAP_FINE | MAP_COARSE)) +#define IS_SINGLE(a) ((a) & MAP_SINGLE) + +//since ola seems to immediately loop back any sent data as input, we only use one buffer +//to avoid excessive event feedback loops +typedef struct /*_ola_universe_model*/ { +	uint8_t data[512]; +	uint16_t map[512]; +} ola_universe; + +typedef struct /*_ola_instance_model*/ { +	/*TODO does ola support remote connections?*/ +	unsigned int universe_id; +	ola_universe data; +} ola_instance_data; diff --git a/backends/ola.md b/backends/ola.md new file mode 100644 index 0000000..e3a1197 --- /dev/null +++ b/backends/ola.md @@ -0,0 +1,41 @@ +### The `ola` backend + +This backend connects the MIDIMonster to the Open Lighting Architecture daemon. This can be useful +to take advantage of additional protocols implemented in OLA. This backend is currently marked as +optional and is only built with `make full` in the `backends/` directory, as the OLA is a large +dependency to require for all users. + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option	| Example value		| Default value | Description						| +|---------------|-----------------------|---------------|-------------------------------------------------------| +| `universe`	| `7`			| `0`		| OLA universe to send/receive data on			| + +#### Channel specification + +A channel is specified by it's universe index. Channel indices start at 1 and end at 512. + +Example mapping: +``` +ola1.231 < in2.123 +``` + +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax +``` +ola1.1+2 > net2.5+123 +``` + +A normal channel that is part of a wide channel can not be mapped individually. + +#### Known bugs / problems + +The backend currently assumes that the OLA daemon is running on the same host as the MIDIMonster. +This may be made configurable in the future. + +This backend requires `libola-dev` to be installed, which pulls in a rather large and aggressive (in terms of probing +and taking over connected hardware) daemon. It is thus marked as optional and only built when executing the `full` target +within the `backends` directory.
\ No newline at end of file diff --git a/backends/osc.c b/backends/osc.c index 5f94ec2..757ad89 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -1,9 +1,8 @@  #include <string.h> -#include <unistd.h>  #include <ctype.h> -#include <netdb.h>  #include <errno.h> -#include <fcntl.h> + +#include "libmmbackend.h"  #include "osc.h"  /* @@ -14,19 +13,30 @@  #define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4)  #define BACKEND_NAME "osc" -int init(){ +static struct { +	uint8_t detect; +} osc_global_config = { +	.detect = 0 +}; + +MM_PLUGIN_API int init(){  	backend osc = {  		.name = BACKEND_NAME, -		.conf = backend_configure, -		.create = backend_instance, -		.conf_instance = backend_configure_instance, -		.channel = backend_channel, -		.handle = backend_set, -		.process = backend_handle, -		.start = backend_start, -		.shutdown = backend_shutdown +		.conf = osc_configure, +		.create = osc_instance, +		.conf_instance = osc_configure_instance, +		.channel = osc_map_channel, +		.handle = osc_set, +		.process = osc_handle, +		.start = osc_start, +		.shutdown = osc_shutdown  	}; +	if(sizeof(osc_channel_ident) != sizeof(uint64_t)){ +		fprintf(stderr, "OSC channel identification union out of bounds\n"); +		return 1; +	} +  	//register backend  	if(mm_backend_register(osc)){  		fprintf(stderr, "Failed to register OSC backend\n"); @@ -36,6 +46,7 @@ int init(){  }  static size_t osc_data_length(osc_parameter_type t){ +	//binary representation lengths for osc data types  	switch(t){  		case int32:  		case float32: @@ -50,6 +61,7 @@ static size_t osc_data_length(osc_parameter_type t){  }  static inline void osc_defaults(osc_parameter_type t, osc_parameter_value* max, osc_parameter_value* min){ +	//data type default ranges  	memset(max, 0, sizeof(osc_parameter_value));  	memset(min, 0, sizeof(osc_parameter_value));  	switch(t){ @@ -72,6 +84,7 @@ static inline void osc_defaults(osc_parameter_type t, osc_parameter_value* max,  }  static inline osc_parameter_value osc_parse(osc_parameter_type t, uint8_t* data){ +	//read value from binary representation  	osc_parameter_value v = {0};  	switch(t){  		case int32: @@ -89,6 +102,7 @@ static inline osc_parameter_value osc_parse(osc_parameter_type t, uint8_t* data)  }  static inline int osc_deparse(osc_parameter_type t, osc_parameter_value v, uint8_t* data){ +	//write value to binary representation  	uint64_t u64 = 0;  	uint32_t u32 = 0;  	switch(t){ @@ -110,6 +124,7 @@ static inline int osc_deparse(osc_parameter_type t, osc_parameter_value v, uint8  }  static inline osc_parameter_value osc_parse_value_spec(osc_parameter_type t, char* value){ +	//read value from string  	osc_parameter_value v = {0};  	switch(t){  		case int32: @@ -131,6 +146,7 @@ static inline osc_parameter_value osc_parse_value_spec(osc_parameter_type t, cha  }  static inline channel_value osc_parameter_normalise(osc_parameter_type t, osc_parameter_value min, osc_parameter_value max, osc_parameter_value cur){ +	//normalise osc value wrt given min/max  	channel_value v = {  		.raw = {0},  		.normalised = 0 @@ -168,18 +184,13 @@ static inline channel_value osc_parameter_normalise(osc_parameter_type t, osc_pa  			fprintf(stderr, "Invalid OSC type passed to interpolation routine\n");  	} -	//fix overshoot -	if(v.normalised > 1.0){ -		v.normalised = 1.0; -	} -	else if(v.normalised < 0.0){ -		v.normalised = 0.0; -	} - +	//clamp to range +	v.normalised = clamp(v.normalised, 1.0, 0.0);  	return v;  }  static inline osc_parameter_value osc_parameter_denormalise(osc_parameter_type t, osc_parameter_value min, osc_parameter_value max, channel_value cur){ +	//convert normalised value to osc value wrt given min/max  	osc_parameter_value v = {0};  	union { @@ -213,167 +224,282 @@ static inline osc_parameter_value osc_parameter_denormalise(osc_parameter_type t  	return v;  } -static int osc_generate_event(channel* c, osc_channel* info, char* fmt, uint8_t* data, size_t data_len){ -	size_t p, off = 0; -	if(!c || !info){ -		return 0; -	} - -	osc_parameter_value min, max, cur; -	channel_value evt; - -	if(!fmt || !data || data_len % 4 || !*fmt){ -		fprintf(stderr, "Invalid OSC packet, data length %zu\n", data_len); +static int osc_path_validate(char* path, uint8_t allow_patterns){ +	//validate osc path or pattern +	char illegal_chars[] = " #,"; +	char pattern_chars[] = "?[]{}*"; +	size_t u, c; +	uint8_t square_open = 0, curly_open = 0; +	 +	if(path[0] != '/'){ +		fprintf(stderr, "%s is not a valid OSC path: Missing root /\n", path);  		return 1;  	} -	//find offset for this parameter -	for(p = 0; p < info->param_index; p++){ -		off += osc_data_length(fmt[p]); -	} +	for(u = 0; u < strlen(path); u++){ +		for(c = 0; c < sizeof(illegal_chars); c++){ +			if(path[u] == illegal_chars[c]){ +				fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, illegal_chars[c], u); +				return 1; +			} +		} -	if(info->type != not_set){ -		max = info->max; -		min = info->min; -	} -	else{ -		osc_defaults(fmt[info->param_index], &max, &min); -	} +		if(!isgraph(path[u])){ +			fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); +			return 1; +		} -	cur = osc_parse(fmt[info->param_index], data + off); -	evt = osc_parameter_normalise(fmt[info->param_index], min, max, cur); +		if(!allow_patterns){ +			for(c = 0; c < sizeof(pattern_chars); c++){ +				if(path[u] == pattern_chars[c]){ +					fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); +					return 1; +				} +			} +		} -	return mm_channel_event(c, evt); -} +		switch(path[u]){ +			case '{': +				if(square_open || curly_open){ +					fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); +					return 1; +				} +				curly_open = 1; +				break; +			case '[': +				if(square_open || curly_open){ +					fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); +					return 1; +				} +				square_open = 1; +				break; +			case '}': +				curly_open = 0; +				break; +			case ']': +				square_open = 0; +				break; +			case '/': +				if(square_open || curly_open){ +					fprintf(stderr, "%s is not a valid OSC path: Pattern across part boundaries\n", path); +					return 1; +				} +		} +	} -static int osc_validate_path(char* path){ -	if(path[0] != '/'){ -		fprintf(stderr, "%s is not a valid OSC path: Missing root /\n", path); +	if(square_open || curly_open){ +		fprintf(stderr, "%s is not a valid OSC path: Unterminated pattern expression\n", path);  		return 1;  	}  	return 0;  } -static int osc_separate_hostspec(char* in, char** host, char** port){ -	size_t u; - -	if(!in || !host || !port){ -		return 1; -	} +static int osc_path_match(char* pattern, char* path){ +	size_t u, p = 0, match_begin, match_end; +	uint8_t match_any = 0, inverted, match; -	for(u = 0; in[u] && !isspace(in[u]); u++){ -	} +	for(u = 0; u < strlen(path); u++){ +		switch(pattern[p]){ +			case '/': +				if(match_any){ +					for(; path[u] && path[u] != '/'; u++){ +					} +				} +				if(path[u] != '/'){ +					return 0; +				} +				match_any = 0; +				p++; +				break; +			case '?': +				match_any = 0; +				p++; +				break; +			case '*': +				match_any = 1; +				p++; +				break; +			case '[': +				inverted = (pattern[p + 1] == '!') ? 1 : 0; +				match_end = match_begin = inverted ? p + 2 : p + 1; +				match = 0; +				for(; pattern[match_end] != ']'; match_end++){ +					if(pattern[match_end] == path[u]){ +						match = 1; +						break; +					} -	//guess -	*host = in; +					if(pattern[match_end + 1] == '-' && pattern[match_end + 2] != ']'){ +						if((pattern[match_end] > pattern[match_end + 2]  +									&& path[u] >= pattern[match_end + 2] +									&& path[u] <= pattern[match_end]) +								|| (pattern[match_end] <= pattern[match_end + 2] +									&& path[u] >= pattern[match_end] +									&& path[u] <= pattern[match_end + 2])){ +							match = 1; +							break; +						} +						match_end += 2; +					} -	if(in[u]){ -		in[u] = 0; -		*port = in + u + 1; -	} -	else{ -		//no port given -		*port = NULL; -	} -	return 0; -} +					if(pattern[match_end + 1] == ']' && match_any && !match +							&& path[u + 1] && path[u + 1] != '/'){ +						match_end = match_begin - 1; +						u++; +					} +				} -static int osc_listener(char* host, char* port){ -	int fd = -1, status, yes = 1, flags; -	struct addrinfo hints = { -		.ai_family = AF_UNSPEC, -		.ai_socktype = SOCK_DGRAM, -		.ai_flags = AI_PASSIVE -	}; -	struct addrinfo* info; -	struct addrinfo* addr_it; +				if(match == inverted){ +					return 0; +				} -	status = getaddrinfo(host, port, &hints, &info); -	if(status){ -		fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); -		return -1; -	} +				match_any = 0; +				//advance to end of pattern +				for(; pattern[p] != ']'; p++){ +				} +				p++; +				break; +			case '{': +				for(match_begin = p + 1; pattern[match_begin] != '}'; match_begin++){ +					//find end +					for(match_end = match_begin; pattern[match_end] != ',' && pattern[match_end] != '}'; match_end++){ +					} -	for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ -		fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); -		if(fd < 0){ -			continue; -		} +					if(!strncmp(path + u, pattern + match_begin, match_end - match_begin)){ +						//advance pattern +						for(; pattern[p] != '}'; p++){ +						} +						p++; +						//advance path +						u += match_end - match_begin - 1; +						break; +					} -		yes = 1; -		if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ -			fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); +					if(pattern[match_end] == '}'){ +						//retry with next if in match_any +						if(match_any && path[u + 1] && path[u + 1] != '/'){ +							u++; +							match_begin = p; +							continue; +						} +						return 0; +					} +					match_begin = match_end; +				} +				match_any = 0; +				break; +			case 0: +				if(match_any){ +					for(; path[u] && path[u] != '/'; u++){ +					} +				} +				if(path[u]){ +					return 0; +				} +				break; +			default: +				if(match_any){ +					for(; path[u] && path[u] != '/' && path[u] != pattern[p]; u++){ +					} +				} +				if(pattern[p] != path[u]){ +					return 0; +				} +				p++; +				break;  		} +	} +	return 1; +} -		yes = 1; -		if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ -			fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); +static int osc_configure(char* option, char* value){ +	if(!strcmp(option, "detect")){ +		osc_global_config.detect = 1; +		if(!strcmp(value, "off")){ +			osc_global_config.detect = 0;  		} +		return 0; +	} -		yes = 0; -		if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ -			fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); -		} +	fprintf(stderr, "Unknown configuration parameter %s for OSC backend\n", option); +	return 1; +} -		status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); -		if(status < 0){ -			close(fd); -			continue; -		} +static int osc_register_pattern(osc_instance_data* data, char* pattern_path, char* configuration){ +	size_t u, pattern; +	char* format = NULL, *token = NULL; -		break; +	if(osc_path_validate(pattern_path, 1)){ +		fprintf(stderr, "Not a valid OSC pattern: %s\n", pattern_path); +		return 1;  	} -	freeaddrinfo(info); - -	if(!addr_it){ -		fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); -		return -1; +	//tokenize configuration +	format = strtok(configuration, " "); +	if(!format || strlen(format) < 1){ +		fprintf(stderr, "Not a valid format specification for OSC pattern %s\n", pattern_path); +		return 1;  	} -	//set nonblocking -	flags = fcntl(fd, F_GETFL, 0); -	if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ -		close(fd); -		fprintf(stderr, "Failed to set OSC descriptor nonblocking\n"); -		return -1; +	//create pattern +	data->pattern = realloc(data->pattern, (data->patterns + 1) * sizeof(osc_channel)); +	if(!data->pattern){ +		fprintf(stderr, "Failed to allocate memory\n"); +		return 1;  	} +	pattern = data->patterns; -	return fd; -} - -static int osc_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ -	struct addrinfo* head; -	struct addrinfo hints = { -		.ai_family = AF_UNSPEC, -		.ai_socktype = SOCK_DGRAM -	}; +	data->pattern[pattern].params = strlen(format); +	data->pattern[pattern].path = strdup(pattern_path); +	data->pattern[pattern].type = calloc(strlen(format), sizeof(osc_parameter_type)); +	data->pattern[pattern].max = calloc(strlen(format), sizeof(osc_parameter_value)); +	data->pattern[pattern].min = calloc(strlen(format), sizeof(osc_parameter_value)); -	int error = getaddrinfo(host, port, &hints, &head); -	if(error || !head){ -		fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); +	if(!data->pattern[pattern].path +			|| !data->pattern[pattern].type +			|| !data->pattern[pattern].max +			|| !data->pattern[pattern].min){ +		//this should fail config parsing and thus call the shutdown function, +		//which should properly free the rest of the data +		fprintf(stderr, "Failed to allocate memory\n");  		return 1;  	} -	memcpy(addr, head->ai_addr, head->ai_addrlen); -	*len = head->ai_addrlen; +	//check format validity and store min/max values +	for(u = 0; u < strlen(format); u++){ +		if(!osc_data_length(format[u])){ +			fprintf(stderr, "Invalid format specifier %c for pattern %s\n", format[u], pattern_path); +			return 1; +		} -	freeaddrinfo(head); -	return 0; -} +		data->pattern[pattern].type[u] = format[u]; -static int backend_configure(char* option, char* value){ -	fprintf(stderr, "The OSC backend does not take any global configuration\n"); -	return 1; +		//parse min/max values +		token = strtok(NULL, " "); +		if(!token){ +			fprintf(stderr, "Missing minimum specification for parameter %" PRIsize_t " of OSC pattern %s\n", u, pattern_path); +			return 1; +		} +		data->pattern[pattern].min[u] = osc_parse_value_spec(format[u], token); + +		token = strtok(NULL, " "); +		if(!token){ +			fprintf(stderr, "Missing maximum specification for parameter %" PRIsize_t " of OSC pattern %s\n", u, pattern_path); +			return 1; +		} +		data->pattern[pattern].max[u] = osc_parse_value_spec(format[u], token); +	} + +	data->patterns++; +	return 0;  } -static int backend_configure_instance(instance* inst, char* option, char* value){ -	osc_instance* data = (osc_instance*) inst->impl; -	char* host = NULL, *port = NULL, *token = NULL, *format = NULL; -	size_t u, p; +static int osc_configure_instance(instance* inst, char* option, char* value){ +	osc_instance_data* data = (osc_instance_data*) inst->impl; +	char* host = NULL, *port = NULL;  	if(!strcmp(option, "root")){ -		if(osc_validate_path(value)){ +		if(osc_path_validate(value, 0)){  			fprintf(stderr, "Not a valid OSC root: %s\n", value);  			return 1;  		} @@ -390,12 +516,13 @@ static int backend_configure_instance(instance* inst, char* option, char* value)  		return 0;  	}  	else if(!strcmp(option, "bind")){ -		if(osc_separate_hostspec(value, &host, &port)){ +		mmbackend_parse_hostspec(value, &host, &port); +		if(!host || !port){  			fprintf(stderr, "Invalid bind address for instance %s\n", inst->name);  			return 1;  		} -		data->fd = osc_listener(host, port); +		data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1);  		if(data->fd < 0){  			fprintf(stderr, "Failed to bind for instance %s\n", inst->name);  			return 1; @@ -413,101 +540,33 @@ static int backend_configure_instance(instance* inst, char* option, char* value)  			return 0;  		} -		if(osc_separate_hostspec(value, &host, &port)){ +		mmbackend_parse_hostspec(value, &host, &port); +		if(!host || !port){  			fprintf(stderr, "Invalid destination address for instance %s\n", inst->name);  			return 1;  		} -		if(osc_parse_addr(host, port, &data->dest, &data->dest_len)){ +		if(mmbackend_parse_sockaddr(host, port, &data->dest, &data->dest_len)){  			fprintf(stderr, "Failed to parse destination address for instance %s\n", inst->name);  			return 1;  		}  		return 0;  	}  	else if(*option == '/'){ -		//pre-configure channel -		if(osc_validate_path(option)){ -			fprintf(stderr, "Not a valid OSC path: %s\n", option); -			return 1; -		} - -		for(u = 0; u < data->channels; u++){ -			if(!strcmp(option, data->channel[u].path)){ -				fprintf(stderr, "OSC channel %s already configured\n", option); -				return 1; -			} -		} - -		//tokenize configuration -		format = strtok(value, " "); -		if(!format || strlen(format) < 1){ -			fprintf(stderr, "Not a valid format for OSC path %s\n", option); -			return 1; -		} - -		//check format validity, create subchannels -		for(p = 0; p < strlen(format); p++){ -			if(!osc_data_length(format[p])){ -				fprintf(stderr, "Invalid format specifier %c for path %s, ignoring\n", format[p], option); -				continue; -			} - -			//register new sub-channel -			data->channel = realloc(data->channel, (data->channels + 1) * sizeof(osc_channel)); -			if(!data->channel){ -				fprintf(stderr, "Failed to allocate memory\n"); -				return 1; -			} - -			memset(data->channel + data->channels, 0, sizeof(osc_channel)); -			data->channel[data->channels].params = strlen(format); -			data->channel[data->channels].param_index = p; -			data->channel[data->channels].type = format[p]; -			data->channel[data->channels].path = strdup(option); - -			if(!data->channel[data->channels].path){ -				fprintf(stderr, "Failed to allocate memory\n"); -				return 1; -			} - -			//parse min/max values -			token = strtok(NULL, " "); -			if(!token){ -				fprintf(stderr, "Missing minimum specification for parameter %zu of %s\n", p, option); -				return 1; -			} -			data->channel[data->channels].min = osc_parse_value_spec(format[p], token); - -			token = strtok(NULL, " "); -			if(!token){ -				fprintf(stderr, "Missing maximum specification for parameter %zu of %s\n", p, option); -				return 1; -			} -			data->channel[data->channels].max = osc_parse_value_spec(format[p], token); - -			//allocate channel from core -			if(!mm_channel(inst, data->channels, 1)){ -				fprintf(stderr, "Failed to register core channel\n"); -				return 1; -			} - -			//increase channel count -			data->channels++; -		} -		return 0; +		return osc_register_pattern(data, option, value);  	} -	fprintf(stderr, "Unknown configuration parameter %s for OSC backend\n", option); +	fprintf(stderr, "Unknown configuration parameter %s for OSC instance %s\n", option, inst->name);  	return 1;  } -static instance* backend_instance(){ +static instance* osc_instance(){  	instance* inst = mm_instance();  	if(!inst){  		return NULL;  	} -	osc_instance* data = calloc(1, sizeof(osc_instance)); +	osc_instance_data* data = calloc(1, sizeof(osc_instance_data));  	if(!data){  		fprintf(stderr, "Failed to allocate memory\n");  		return NULL; @@ -518,32 +577,39 @@ static instance* backend_instance(){  	return inst;  } -static channel* backend_channel(instance* inst, char* spec){ -	size_t u; -	osc_instance* data = (osc_instance*) inst->impl; -	size_t param_index = 0; +static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags){ +	size_t u, p; +	osc_instance_data* data = (osc_instance_data*) inst->impl; +	osc_channel_ident ident = { +		.label = 0 +	};  	//check spec for correctness -	if(osc_validate_path(spec)){ +	if(osc_path_validate(spec, 0)){  		return NULL;  	}  	//parse parameter offset  	if(strrchr(spec, ':')){ -		param_index = strtoul(strrchr(spec, ':') + 1, NULL, 10); +		ident.fields.parameter = strtoul(strrchr(spec, ':') + 1, NULL, 10);  		*(strrchr(spec, ':')) = 0;  	}  	//find matching channel  	for(u = 0; u < data->channels; u++){ -		if(!strcmp(spec, data->channel[u].path) && data->channel[u].param_index == param_index){ -			//fprintf(stderr, "Reusing previously created channel %s parameter %zu\n", data->channel[u].path, data->channel[u].param_index); +		if(!strcmp(spec, data->channel[u].path)){  			break;  		}  	}  	//allocate new channel  	if(u == data->channels){ +		for(p = 0; p < data->patterns; p++){ +			if(osc_path_match(data->pattern[p].path, spec)){ +				break; +			} +		} +  		data->channel = realloc(data->channel, (u + 1) * sizeof(osc_channel));  		if(!data->channel){  			fprintf(stderr, "Failed to allocate memory\n"); @@ -551,132 +617,211 @@ static channel* backend_channel(instance* inst, char* spec){  		}  		memset(data->channel + u, 0, sizeof(osc_channel)); -		data->channel[u].param_index = param_index;  		data->channel[u].path = strdup(spec); +		if(p != data->patterns){ +			fprintf(stderr, "Matched pattern %s for %s\n", data->pattern[p].path, spec); +			data->channel[u].params = data->pattern[p].params; +			//just reuse the pointers from the pattern +			data->channel[u].type = data->pattern[p].type; +			data->channel[u].max = data->pattern[p].max; +			data->channel[u].min = data->pattern[p].min; + +			//these are per channel +			data->channel[u].in = calloc(data->channel[u].params, sizeof(osc_parameter_value)); +			data->channel[u].out = calloc(data->channel[u].params, sizeof(osc_parameter_value)); +		} +		else if(data->patterns){ +			fprintf(stderr, "No pattern match found for %s\n", spec); +		} -		if(!data->channel[u].path){ +		if(!data->channel[u].path +				|| (data->channel[u].params && (!data->channel[u].in || !data->channel[u].out))){  			fprintf(stderr, "Failed to allocate memory\n");  			return NULL;  		}  		data->channels++;  	} -	return mm_channel(inst, u, 1); +	ident.fields.channel = u; +	return mm_channel(inst, ident.label, 1);  } -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v){ -	uint8_t xmit_buf[OSC_XMIT_BUF], *format = NULL; -	size_t evt = 0, off, members, p; +static int osc_output_channel(instance* inst, size_t channel){ +	osc_instance_data* data = (osc_instance_data*) inst->impl; +	uint8_t xmit_buf[OSC_XMIT_BUF] = "", *format = NULL; +	size_t offset = 0, p; + +	//fix destination rport if required +	if(data->forced_rport){ +		//cheating a bit because both IPv4 and IPv6 have the port at the same offset +		struct sockaddr_in* sockadd = (struct sockaddr_in*) &(data->dest); +		sockadd->sin_port = htobe16(data->forced_rport); +	} + +	//determine minimum packet size +	if(osc_align((data->root ? strlen(data->root) : 0) + strlen(data->channel[channel].path) + 1) + osc_align(data->channel[channel].params + 2)  >= sizeof(xmit_buf)){ +		fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s\n", inst->name, data->channel[channel].path); +		return 1; +	} + +	//copy osc target path +	if(data->root){ +		memcpy(xmit_buf, data->root, strlen(data->root)); +		offset += strlen(data->root); +	} +	 +	memcpy(xmit_buf + offset, data->channel[channel].path, strlen(data->channel[channel].path)); +	offset += strlen(data->channel[channel].path) + 1; +	offset = osc_align(offset); + +	//get format string offset, initialize +	format = xmit_buf + offset; +	offset += osc_align(data->channel[channel].params + 2); +	*format = ','; +	format++; + +	for(p = 0; p < data->channel[channel].params; p++){ +		//write format specifier +		format[p] = data->channel[channel].type[p]; + +		//write data +		if(offset + osc_data_length(data->channel[channel].type[p]) >= sizeof(xmit_buf)){ +			fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %" PRIsize_t "\n", inst->name, data->channel[channel].path, p); +			return 1; +		} + +		osc_deparse(data->channel[channel].type[p], +				data->channel[channel].out[p], +				xmit_buf + offset); +		offset += osc_data_length(data->channel[channel].type[p]); +	} + +	//output packet +	if(sendto(data->fd, xmit_buf, offset, 0, (struct sockaddr*) &(data->dest), data->dest_len) < 0){ +		fprintf(stderr, "Failed to transmit OSC packet: %s\n", strerror(errno)); +	} +	return 0; +} + +static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ +	size_t evt = 0, mark = 0; +	int rv = 0; +	osc_channel_ident ident = { +		.label = 0 +	}; +	osc_parameter_value current; +  	if(!num){  		return 0;  	} -	osc_instance* data = (osc_instance*) inst->impl; +	osc_instance_data* data = (osc_instance_data*) inst->impl;  	if(!data->dest_len){ -		fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%zu channels)\n", inst->name, num); +		fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%" PRIsize_t " channels)\n", inst->name, num);  		return 0;  	}  	for(evt = 0; evt < num; evt++){ -		off = c[evt]->ident; +		ident.label = c[evt]->ident;  		//sanity check -		if(off >= data->channels){ +		if(ident.fields.channel >= data->channels +				|| ident.fields.parameter >= data->channel[ident.fields.channel].params){  			fprintf(stderr, "OSC channel identifier out of range\n");  			return 1;  		}  		//if the format is unknown, don't output -		if(data->channel[off].type == not_set || data->channel[off].params == 0){ -			fprintf(stderr, "OSC channel %s.%s requires format specification for output\n", inst->name, data->channel[off].path); +		if(!data->channel[ident.fields.channel].params){ +			fprintf(stderr, "OSC channel %s.%s requires format specification for output\n", inst->name, data->channel[ident.fields.channel].path);  			continue;  		} -		//update current value -		data->channel[off].current = osc_parameter_denormalise(data->channel[off].type, data->channel[off].min, data->channel[off].max, v[evt]); -		//mark channel -		data->channel[off].mark = 1; +		//only output on change +		current = osc_parameter_denormalise(data->channel[ident.fields.channel].type[ident.fields.parameter], +				data->channel[ident.fields.channel].min[ident.fields.parameter], +				data->channel[ident.fields.channel].max[ident.fields.parameter], +				v[evt]); +		if(memcmp(¤t, &data->channel[ident.fields.channel].out[ident.fields.parameter], sizeof(current))){ +			//update current value +			data->channel[ident.fields.channel].out[ident.fields.parameter] = current; +			//mark channel +			data->channel[ident.fields.channel].mark = 1; +			mark = 1; +		}  	} - -	//fix destination rport if required -	if(data->forced_rport){ -		//cheating a bit because both IPv4 and IPv6 have the port at the same offset -		struct sockaddr_in* sockadd = (struct sockaddr_in*) &(data->dest); -		sockadd->sin_port = htobe16(data->forced_rport); +	 +	if(mark){ +		//output all marked channels +		for(evt = 0; !rv && evt < num; evt++){ +			ident.label = c[evt]->ident; +			if(data->channel[ident.fields.channel].mark){ +				rv |= osc_output_channel(inst, ident.fields.channel); +				data->channel[ident.fields.channel].mark = 0; +			} +		}  	} +	return rv; +} -	//find all marked channels -	for(evt = 0; evt < data->channels; evt++){ -		//zero output buffer -		memset(xmit_buf, 0, sizeof(xmit_buf)); -		if(data->channel[evt].mark){ -			//determine minimum packet size -			if(osc_align((data->root ? strlen(data->root) : 0) + strlen(data->channel[evt].path) + 1) + osc_align(data->channel[evt].params + 2)  >= sizeof(xmit_buf)){ -				fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s\n", inst->name, data->channel[evt].path); -				return 1; -			} +static int osc_process_packet(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){ +	osc_instance_data* data = (osc_instance_data*) inst->impl; +	size_t c, p, offset = 0; +	osc_parameter_value min, max, cur; +	channel_value evt; +	osc_channel_ident ident = { +		.label = 0 +	}; +	channel* chan = NULL; -			off = 0; -			//copy osc target path -			if(data->root){ -				memcpy(xmit_buf, data->root, strlen(data->root)); -				off += strlen(data->root); -			} -			memcpy(xmit_buf + off, data->channel[evt].path, strlen(data->channel[evt].path)); -			off += strlen(data->channel[evt].path) + 1; -			off = osc_align(off); - -			//get format string offset, initialize -			format = xmit_buf + off; -			off += osc_align(data->channel[evt].params + 2); -			*format = ','; -			format++; - -			//gather subchannels, unmark -			members = 0; -			for(p = 0; p < data->channels && members < data->channel[evt].params; p++){ -				if(!strcmp(data->channel[evt].path, data->channel[p].path)){ -					//unmark channel -					data->channel[p].mark = 0; - -					//sanity check -					if(data->channel[p].param_index >= data->channel[evt].params){ -						fprintf(stderr, "OSC channel %s.%s has multiple parameter offset definitions\n", inst->name, data->channel[evt].path); -						return 1; -					} +	if(payload_len % 4){ +		fprintf(stderr, "Invalid OSC packet, data length %" PRIsize_t "\n", payload_len); +		return 0; +	} -					//write format specifier -					format[data->channel[p].param_index] = data->channel[p].type; +	for(c = 0; c < data->channels; c++){ +		if(!strcmp(local_path, data->channel[c].path)){ +			ident.fields.channel = c; +			//unconfigured input should work without errors (using default limits) +			if(data->channel[c].params && strlen(format) != data->channel[c].params){ +				fprintf(stderr, "OSC message %s.%s had format %s, internal representation has %" PRIsize_t " parameters\n", inst->name, local_path, format, data->channel[c].params); +				continue; +			} -					//write data -					//FIXME this currently depends on all channels being registered in the correct order, since it just appends data -					if(off + osc_data_length(data->channel[p].type) >= sizeof(xmit_buf)){ -						fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %zu\n", inst->name, data->channel[evt].path, members); -						return 1; +			for(p = 0; p < strlen(format); p++){ +				ident.fields.parameter = p; +				if(data->channel[c].params){ +					max = data->channel[c].max[p]; +					min = data->channel[c].min[p]; +				} +				else{ +					osc_defaults(format[p], &max, &min); +				} +				cur = osc_parse(format[p], payload + offset); +				if(!data->channel[c].params || memcmp(&cur, &data->channel[c].in, sizeof(cur))){ +					evt = osc_parameter_normalise(format[p], min, max, cur); +					chan = mm_channel(inst, ident.label, 0); +					if(chan){ +						mm_channel_event(chan, evt);  					} - -					osc_deparse(data->channel[p].type, data->channel[p].current, xmit_buf + off); -					off += osc_data_length(data->channel[p].type); -					members++;  				} -			} -			//output packet -			if(sendto(data->fd, xmit_buf, off, 0, (struct sockaddr*) &(data->dest), data->dest_len) < 0){ -				fprintf(stderr, "Failed to transmit OSC packet: %s\n", strerror(errno)); +				//skip to next parameter data +				offset += osc_data_length(format[p]); +				//TODO check offset against payload length  			}  		}  	} +  	return 0;  } -static int backend_handle(size_t num, managed_fd* fds){ +static int osc_handle(size_t num, managed_fd* fds){  	size_t fd;  	char recv_buf[OSC_RECV_BUF];  	instance* inst = NULL; -	osc_instance* data = NULL; +	osc_instance_data* data = NULL;  	ssize_t bytes_read = 0; -	size_t c;  	char* osc_fmt = NULL;  	char* osc_local = NULL;  	uint8_t* osc_data = NULL; @@ -688,7 +833,7 @@ static int backend_handle(size_t num, managed_fd* fds){  			continue;  		} -		data = (osc_instance*) inst->impl; +		data = (osc_instance_data*) inst->impl;  		do{  			if(data->learn){ @@ -698,16 +843,17 @@ static int backend_handle(size_t num, managed_fd* fds){  			else{  				bytes_read = recv(fds[fd].fd, recv_buf, sizeof(recv_buf), 0);  			} + +			if(bytes_read <= 0){ +				break; +			} +  			if(data->root && strncmp(recv_buf, data->root, min(bytes_read, strlen(data->root)))){  				//ignore packet for different root  				continue;  			}  			osc_local = recv_buf + (data->root ? strlen(data->root) : 0); -			if(bytes_read < 0){ -				break; -			} -  			osc_fmt = recv_buf + osc_align(strlen(recv_buf) + 1);  			if(*osc_fmt != ','){  				//invalid format string @@ -716,24 +862,23 @@ static int backend_handle(size_t num, managed_fd* fds){  			}  			osc_fmt++; -			osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); +			if(osc_global_config.detect){ +				fprintf(stderr, "Incoming OSC data: Path %s.%s Format %s\n", inst->name, osc_local, osc_fmt); +			} +  			//FIXME check supplied data length +			osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); -			for(c = 0; c < data->channels; c++){ -				//FIXME implement proper OSC path match -				//prefix match -				if(!strcmp(osc_local, data->channel[c].path)){ -					if(strlen(osc_fmt) > data->channel[c].param_index){ -						//fprintf(stderr, "Taking parameter %zu of %s (%s), %zd bytes, data offset %zu\n", data->channel[c].param_index, recv_buf, osc_fmt, bytes_read, (osc_data - (uint8_t*)recv_buf)); -						if(osc_generate_event(mm_channel(inst, c, 0), data->channel + c, osc_fmt, osc_data, bytes_read - (osc_data - (uint8_t*) recv_buf))){ -							fprintf(stderr, "Failed to generate OSC channel event\n"); -						} -					} -				} +			if(osc_process_packet(inst, osc_local, osc_fmt, osc_data, bytes_read - (osc_data - (uint8_t*) recv_buf))){ +				return 1;  			}  		} while(bytes_read > 0); +		#ifdef _WIN32 +		if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){ +		#else  		if(bytes_read < 0 && errno != EAGAIN){ +		#endif  			fprintf(stderr, "OSC failed to receive data for instance %s: %s\n", inst->name, strerror(errno));  		} @@ -746,10 +891,10 @@ static int backend_handle(size_t num, managed_fd* fds){  	return 0;  } -static int backend_start(){ +static int osc_start(){  	size_t n, u, fds = 0;  	instance** inst = NULL; -	osc_instance* data = NULL; +	osc_instance_data* data = NULL;  	//fetch all instances  	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ @@ -764,7 +909,7 @@ static int backend_start(){  	//update instance identifiers  	for(u = 0; u < n; u++){ -		data = (osc_instance*) inst[u]->impl; +		data = (osc_instance_data*) inst[u]->impl;  		if(data->fd >= 0){  			inst[u]->ident = data->fd; @@ -780,16 +925,16 @@ static int backend_start(){  		}  	} -	fprintf(stderr, "OSC backend registered %zu descriptors to core\n", fds); +	fprintf(stderr, "OSC backend registered %" PRIsize_t " descriptors to core\n", fds);  	free(inst);  	return 0;  } -static int backend_shutdown(){ +static int osc_shutdown(){  	size_t n, u, c;  	instance** inst = NULL; -	osc_instance* data = NULL; +	osc_instance_data* data = NULL;  	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){  		fprintf(stderr, "Failed to fetch instance list\n"); @@ -797,20 +942,32 @@ static int backend_shutdown(){  	}  	for(u = 0; u < n; u++){ -		data = (osc_instance*) inst[u]->impl; +		data = (osc_instance_data*) inst[u]->impl;  		for(c = 0; c < data->channels; c++){  			free(data->channel[c].path); +			free(data->channel[c].in); +			free(data->channel[c].out);  		}  		free(data->channel); +		for(c = 0; c < data->patterns; c++){ +			free(data->pattern[c].path); +			free(data->pattern[c].type); +			free(data->pattern[c].min); +			free(data->pattern[c].max); +		} +		free(data->pattern); +  		free(data->root);  		if(data->fd >= 0){  			close(data->fd);  		}  		data->fd = -1;  		data->channels = 0; +		data->patterns = 0;  		free(inst[u]->impl);  	}  	free(inst); +	fprintf(stderr, "OSC backend shut down\n");  	return 0;  } diff --git a/backends/osc.h b/backends/osc.h index 5938f12..6f3b923 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -1,19 +1,21 @@  #include "midimonster.h"  #include <sys/types.h> +#ifndef _WIN32  #include <sys/socket.h> +#endif  #define OSC_RECV_BUF 8192  #define OSC_XMIT_BUF 8192 -int init(); -static int backend_configure(char* option, char* value); -static int backend_configure_instance(instance* instance, char* option, char* value); -static instance* backend_instance(); -static channel* backend_channel(instance* instance, char* spec); -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v); -static int backend_handle(size_t num, managed_fd* fds); -static int backend_start(); -static int backend_shutdown(); +MM_PLUGIN_API int init(); +static int osc_configure(char* option, char* value); +static int osc_configure_instance(instance* inst, char* option, char* value); +static instance* osc_instance(); +static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags); +static int osc_set(instance* inst, size_t num, channel** c, channel_value* v); +static int osc_handle(size_t num, managed_fd* fds); +static int osc_start(); +static int osc_shutdown();  typedef enum {  	not_set = 0, @@ -34,22 +36,42 @@ typedef union {  typedef struct /*_osc_channel*/ {  	char* path;  	size_t params; -	size_t param_index;  	uint8_t mark; -	osc_parameter_type type; -	osc_parameter_value max; -	osc_parameter_value min; -	osc_parameter_value current; +	osc_parameter_type* type; +	osc_parameter_value* max; +	osc_parameter_value* min; +	osc_parameter_value* in; +	osc_parameter_value* out;  } osc_channel;  typedef struct /*_osc_instance_data*/ { +	//pre-configured channel patterns +	size_t patterns; +	osc_channel* pattern; + +	//actual channel registry  	size_t channels;  	osc_channel* channel; + +	//instance config  	char* root; +	uint8_t learn; + +	//peer addressing  	socklen_t dest_len;  	struct sockaddr_storage dest; -	int fd; -	uint8_t learn;  	uint16_t forced_rport; -} osc_instance; + +	//peer fd +	int fd; +} osc_instance_data; + +typedef union { +	struct { +		uint32_t channel; +		uint32_t parameter; +	} fields; +	uint64_t label; +} osc_channel_ident; + diff --git a/backends/osc.md b/backends/osc.md new file mode 100644 index 0000000..1446e06 --- /dev/null +++ b/backends/osc.md @@ -0,0 +1,103 @@ +### The `osc` backend + +This backend offers read and write access to the Open Sound Control protocol, +spoken primarily by visual interface tools and hardware such as TouchOSC. + +#### Global configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `detect`	| `on`			| `off`			| Output the path of all incoming OSC packets to allow for easier configuration. Any path filters configured using the `root` instance configuration options still apply. | + +#### Instance configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `root`	| `/my/osc/path`	| none			| An OSC path prefix to be prepended to all channels | +| `bind`	| `:: 8000`		| none			| The host and port to listen on | +| `destination`	| `10.11.12.13 8001`	| none			| Remote address to send OSC data to. Setting this enables the instance for output. The special value `learn` causes the MIDImonster to always reply to the address the last incoming packet came from. A different remote port for responses can be forced with the syntax `learn@<port>` | + +Note that specifying an instance root speeds up matching, as packets not matching +it are ignored early in processing. + +Channels that are to be output or require a value range different from the default ranges (see below) +require special configuration, as their types and limits have to be set. + +This is done by specifying *patterns* in the instance configuration using an assignment of the syntax + +``` +/local/osc/path = <format> <min> <max> <min> <max> ... +``` + +The pattern will be matched only against the local part (that is, the path excluding any configured instance root). +Patterns may contain the following expressions (conforming to the [OSC pattern matching specification](http://opensoundcontrol.org/spec-1_0)): +* `?` matches any single legal character +* `*` matches zero or more legal characters +* A comma-separated list of strings inside curly braces `{}` matches any of the strings +* A string of characters within square brackets `[]` matches any character in the string +	* Two characters with a `-` between them specify a range of characters +	* An exclamation mark immediately after the opening `[` negates the meaning of the expression (ie. it matches characters not in the range) +* Any other legal character matches only itself + +**format** may be any sequence of valid OSC type characters. See below for a table of supported +OSC types. + +For each component of the path, the minimum and maximum values must be given separated by spaces. +Components may be accessed in the mapping section as detailed in the next section. + +An example configuration for transmission of an OSC message with 2 floating point components with +a range between 0.0 and 2.0 (for example, an X-Y control), would look as follows: + +``` +/1/xy1 = ff 0.0 2.0 0.0 2.0 +``` + +To configure a range of faders, an expression similar to the following line could be used + +``` +/1/fader* = f 0.0 1.0 +``` + +When matching channels against the patterns to use, the first matching pattern (in the order in which they have been configured) will be used +as configuration for that channel. + +#### Channel specification + +A channel may be any valid OSC path, to which the instance root will be prepended if +set. Multi-value controls (such as X-Y pads) are supported by appending `:n` to the path, +where `n` is the parameter index, with the first (and default) one being `0`. + +Example mapping: +``` +osc1./1/xy1:0 > osc2./1/fader1 +``` + +Note that any channel that is to be output will need to be set up in the instance +configuration. + +#### Supported types & value ranges + +OSC allows controls to have individual value ranges and supports different parameter types. +The following types are currently supported by the MIDImonster: + +* **i**: 32-bit signed integer +* **f**: 32-bit IEEE floating point +* **h**: 64-bit signed integer +* **d**: 64-bit double precision floating point + +For each type, there is a default value range which will be assumed if the channel is not otherwise +configured using the instance configuration. Values out of a channels range will be clipped. + +The default ranges are: + +* **i**: `0` to `255` +* **f**: `0.0` to `1.0` +* **h**: `0` to `1024` +* **d**: `0.0` to `1.0` + +#### Known bugs / problems + +The OSC path match currently works on the unit of characters. This may lead to some unexpected results +when matching expressions of the form `*<expr>`. + +Ping requests are not yet answered. There may be some problems using broadcast output and input. diff --git a/backends/sacn.c b/backends/sacn.c index fde8d90..2229b8a 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -1,14 +1,18 @@  #include <string.h>  #include <sys/types.h> -#include <sys/socket.h> -#include <netdb.h>  #include <unistd.h>  #include <errno.h>  #include <fcntl.h>  #include <ctype.h> +#ifndef _WIN32 +#include <netdb.h>  #include <netinet/in.h> +#include <sys/socket.h> +#endif +#include "libmmbackend.h"  #include "sacn.h" +  //upper limit imposed by using the fd index as 16-bit part of the instance id  #define MAX_FDS 4096  #define BACKEND_NAME "sacn" @@ -27,7 +31,7 @@ static struct /*_sacn_global_config*/ {  	.last_announce = 0  }; -int init(){ +MM_PLUGIN_API int init(){  	backend sacn = {  		.name = BACKEND_NAME,  		.conf = sacn_configure, @@ -40,6 +44,11 @@ int init(){  		.shutdown = sacn_shutdown  	}; +	if(sizeof(sacn_instance_id) != sizeof(uint64_t)){ +		fprintf(stderr, "sACN instance identification union out of bounds\n"); +		return 1; +	} +  	//register the backend  	if(mm_backend_register(sacn)){  		fprintf(stderr, "Failed to register sACN backend\n"); @@ -50,68 +59,14 @@ int init(){  }  static int sacn_listener(char* host, char* port, uint8_t fd_flags){ -	int fd = -1, status, yes = 1, flags; -	struct addrinfo hints = { -		.ai_family = AF_UNSPEC, -		.ai_socktype = SOCK_DGRAM, -		.ai_flags = AI_PASSIVE -	}; -	struct addrinfo* info; -	struct addrinfo* addr_it; - +	int fd = -1;  	if(global_cfg.fds >= MAX_FDS){  		fprintf(stderr, "sACN backend descriptor limit reached\n");  		return -1;  	} -	status = getaddrinfo(host, port, &hints, &info); -	if(status){ -		fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); -		return -1; -	} - -	for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ -		fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); -		if(fd < 0){ -			continue; -		} - -		yes = 1; -		if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ -			fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); -		} - -		yes = 1; -		if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ -			fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); -		} - -		yes = 0; -		if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ -			fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); -		} - -		status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); -		if(status < 0){ -			close(fd); -			continue; -		} - -		break; -	} - -	freeaddrinfo(info); - -	if(!addr_it){ -		fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); -		return -1; -	} - -	//set nonblocking -	flags = fcntl(fd, F_GETFL, 0); -	if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ -		fprintf(stderr, "Failed to set sACN descriptor nonblocking\n"); -		close(fd); +	fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); +	if(fd < 0){  		return -1;  	} @@ -123,7 +78,7 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){  		return -1;  	} -	fprintf(stderr, "sACN backend interface %zu bound to %s port %s\n", global_cfg.fds, host, port); +	fprintf(stderr, "sACN backend interface %" PRIsize_t " bound to %s port %s\n", global_cfg.fds, host, port);  	global_cfg.fd[global_cfg.fds].fd = fd;  	global_cfg.fd[global_cfg.fds].flags = fd_flags;  	global_cfg.fd[global_cfg.fds].universes = 0; @@ -133,55 +88,6 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){  	return 0;  } -static int sacn_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ -	struct addrinfo* head; -	struct addrinfo hints = { -		.ai_family = AF_UNSPEC, -		.ai_socktype = SOCK_DGRAM -	}; - -	int error = getaddrinfo(host, port, &hints, &head); -	if(error || !head){ -		fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); -		return 1; -	} - -	memcpy(addr, head->ai_addr, head->ai_addrlen); -	*len = head->ai_addrlen; - -	freeaddrinfo(head); -	return 0; -} - -static int sacn_parse_hostspec(char* in, char** host, char** port, uint8_t* flags){ -	size_t u; - -	if(!in || !host || !port){ -		return 1; -	} - -	for(u = 0; in[u] && !isspace(in[u]); u++){ -	} - -	//guess -	*host = in; - -	if(in[u]){ -		in[u] = 0; -		*port = in + u + 1; -	} -	else{ -		//no port given -		*port = SACN_PORT; -	} - -	if(flags){ -		//TODO parse hostspec trailing data for options -		*flags = 0; -	} -	return 0; -} -  static int sacn_configure(char* option, char* value){  	char* host = NULL, *port = NULL, *next = NULL;  	uint8_t flags = 0; @@ -204,8 +110,13 @@ static int sacn_configure(char* option, char* value){  		}  	}  	else if(!strcmp(option, "bind")){ -		if(sacn_parse_hostspec(value, &host, &port, &flags)){ -			fprintf(stderr, "Not a valid sACN bind address: %s\n", value); +		mmbackend_parse_hostspec(value, &host, &port); +		if(!port){ +			port = SACN_PORT; +		} + +		if(!host){ +			fprintf(stderr, "No valid sACN bind address provided\n");  			return 1;  		} @@ -243,12 +154,17 @@ static int sacn_configure_instance(instance* inst, char* option, char* value){  		return 0;  	}  	else if(!strcmp(option, "destination")){ -		if(sacn_parse_hostspec(value, &host, &port, NULL)){ -			fprintf(stderr, "Not a valid sACN destination for instance %s: %s\n", inst->name, value); +		mmbackend_parse_hostspec(value, &host, &port); +		if(!port){ +			port = SACN_PORT; +		} + +		if(!host){ +			fprintf(stderr, "No valid sACN destination for instance %s\n", inst->name);  			return 1;  		} -		return sacn_parse_addr(host, port, &data->dest_addr, &data->dest_len); +		return mmbackend_parse_sockaddr(host, port, &data->dest_addr, &data->dest_len);  	}  	else if(!strcmp(option, "from")){  		next = value; @@ -282,7 +198,7 @@ static instance* sacn_instance(){  	return inst;  } -static channel* sacn_channel(instance* inst, char* spec){ +static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){  	sacn_instance_data* data = (sacn_instance_data*) inst->impl;  	char* spec_next = spec; @@ -362,7 +278,7 @@ static int sacn_transmit(instance* inst){  	memcpy(pdu.data.source_name, global_cfg.source_name, sizeof(pdu.data.source_name));  	memcpy((((uint8_t*)pdu.data.data) + 1), data->data.out, 512); -	if(sendto(global_cfg.fd[data->fd_index].fd, &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ +	if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){  		fprintf(stderr, "Failed to output sACN frame for instance %s: %s\n", inst->name, strerror(errno));  	} @@ -384,7 +300,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){  	}  	if(!data->xmit_prio){ -		fprintf(stderr, "sACN instance %s not enabled for output (%zu channel events)\n", inst->name, num); +		fprintf(stderr, "sACN instance %s not enabled for output (%" PRIsize_t " channel events)\n", inst->name, num);  		return 0;  	} @@ -469,7 +385,7 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame  			}  			if(!chan){ -				fprintf(stderr, "Active channel %zu on %s not known to core", u, inst->name); +				fprintf(stderr, "Active channel %" PRIsize_t " on %s not known to core", u, inst->name);  				return 1;  			} @@ -536,8 +452,8 @@ static void sacn_discovery(size_t fd){  		pdu.data.page = page;  		memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t)); -		if(sendto(global_cfg.fd[fd].fd, &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ -			fprintf(stderr, "Failed to output sACN universe discovery frame for interface %zu: %s\n", fd, strerror(errno)); +		if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ +			fprintf(stderr, "Failed to output sACN universe discovery frame for interface %" PRIsize_t ": %s\n", fd, strerror(errno));  		}  	}  } @@ -603,7 +519,11 @@ static int sacn_handle(size_t num, managed_fd* fds){  			}  		} while(bytes_read > 0); +		#ifdef _WIN32 +		if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){ +		#else  		if(bytes_read < 0 && errno != EAGAIN){ +		#endif  			fprintf(stderr, "sACN failed to receive data: %s\n", strerror(errno));  		} @@ -668,7 +588,7 @@ static int sacn_start(){  		if(!data->unicast_input){  			mcast_req.imr_multiaddr.s_addr = htobe32(((uint32_t) 0xefff0000) | ((uint32_t) data->uni)); -			if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mcast_req, sizeof(mcast_req))){ +			if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){  				fprintf(stderr, "Failed to join Multicast group for sACN universe %u on instance %s: %s\n", data->uni, inst[u]->name, strerror(errno));  			}  		} @@ -695,7 +615,7 @@ static int sacn_start(){  		}  	} -	fprintf(stderr, "sACN backend registering %zu descriptors to core\n", global_cfg.fds); +	fprintf(stderr, "sACN backend registering %" PRIsize_t " descriptors to core\n", global_cfg.fds);  	for(u = 0; u < global_cfg.fds; u++){  		//allocate memory for storing last frame transmission timestamp  		global_cfg.fd[u].last_frame = calloc(global_cfg.fd[u].universes, sizeof(uint64_t)); diff --git a/backends/sacn.h b/backends/sacn.h index e7106f7..1d3268c 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -1,11 +1,10 @@ -#include <sys/socket.h>  #include "midimonster.h" -int init(); +MM_PLUGIN_API int init();  static int sacn_configure(char* option, char* value);  static int sacn_configure_instance(instance* instance, char* option, char* value);  static instance* sacn_instance(); -static channel* sacn_channel(instance* instance, char* spec); +static channel* sacn_channel(instance* instance, char* spec, uint8_t flags);  static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v);  static int sacn_handle(size_t num, managed_fd* fds);  static int sacn_start(); diff --git a/backends/sacn.md b/backends/sacn.md new file mode 100644 index 0000000..434beeb --- /dev/null +++ b/backends/sacn.md @@ -0,0 +1,58 @@ +### The `sacn` backend + +The sACN backend provides read-write access to the Multicast-UDP based streaming ACN protocol (ANSI E1.31-2016), +used for lighting fixture control. The backend sends universe discovery frames approximately every 10 seconds, +containing all write-enabled universes. + +#### Global configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `name`	| `sACN source`		| `MIDIMonster`		| sACN source name	| +| `cid`		| `0xAA 0xBB 0xCC` ...	| `MIDIMonster`		| Source CID (16 bytes)	| +| `bind`	| `0.0.0.0 5568`	| none			| Binds a network address to listen for data. This option may be set multiple times, with each descriptor being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one descriptor is required for transmission. | + +#### Instance configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `universe`	| `1`			| none			| Universe identifier between 1 and 63999 | +| `interface`	| `1`			| `0`			| The bound address to use for data input/output | +| `priority`	| `100`			| none			| The data priority to transmit for this instance. Setting this option enables the instance for output and includes it in the universe discovery report. | +| `destination`	| `10.2.2.2`		| Universe multicast	| Destination address for unicast output. If unset, the multicast destination for the specified universe is used. | +| `from`	| `0xAA 0xBB` ...	| none			| 16-byte input source CID filter. Setting this option filters the input stream for this universe. | +| `unicast`	| `1`			| `0`			| Prevent this instance from joining its universe multicast group | + +Note that instances accepting multicast input also process unicast frames directed at them, while +instances in `unicast` mode will not receive multicast frames. + +#### Channel specification + +A channel is specified by it's universe index. Channel indices start at 1 and end at 512. + +Example mapping: +``` +sacn1.231 < sacn2.123 +``` + +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax +``` +sacn.1+2 > sacn2.5+123 +``` + +A normal channel that is part of a wide channel can not be mapped individually. + +#### Known bugs / problems + +The DMX start code of transmitted and received universes is fixed as `0`. + +The (upper) limit on packet transmission rate mandated by section 6.6.1 of the sACN specification is disregarded. +The rate of packet transmission is influenced by the rate of incoming mapped events on the instance. + +Universe synchronization is currently not supported, though this feature may be implemented in the future. + +To use multicast input, all networking hardware in the path must support the IGMPv2 protocol. + +The Linux kernel limits the number of multicast groups an interface may join to 20. An instance configured +for input automatically joins the multicast group for its universe, unless configured in `unicast` mode. +This limit can be raised by changing the kernel option in `/proc/sys/net/ipv4/igmp_max_memberships`. diff --git a/backends/winmidi.c b/backends/winmidi.c new file mode 100644 index 0000000..790257b --- /dev/null +++ b/backends/winmidi.c @@ -0,0 +1,603 @@ +#include <string.h> + +#include "libmmbackend.h" +#include <mmsystem.h> + +#include "winmidi.h" + +#define BACKEND_NAME "winmidi" + +static struct { +	uint8_t list_devices; +	uint8_t detect; +	int socket_pair[2]; + +	CRITICAL_SECTION push_events; +	volatile size_t events_alloc; +	volatile size_t events_active; +	volatile winmidi_event* event; +} backend_config = { +	.list_devices = 0, +	.socket_pair = {-1, -1} +}; + +//TODO receive feedback socket until EAGAIN + +MM_PLUGIN_API int init(){ +	backend winmidi = { +		.name = BACKEND_NAME, +		.conf = winmidi_configure, +		.create = winmidi_instance, +		.conf_instance = winmidi_configure_instance, +		.channel = winmidi_channel, +		.handle = winmidi_set, +		.process = winmidi_handle, +		.start = winmidi_start, +		.shutdown = winmidi_shutdown +	}; + +	if(sizeof(winmidi_channel_ident) != sizeof(uint64_t)){ +		fprintf(stderr, "winmidi channel identification union out of bounds\n"); +		return 1; +	} + +	//register backend +	if(mm_backend_register(winmidi)){ +		fprintf(stderr, "Failed to register winmidi backend\n"); +		return 1; +	} + +	//initialize critical section +	InitializeCriticalSectionAndSpinCount(&backend_config.push_events, 4000); +	return 0; +} + +static int winmidi_configure(char* option, char* value){ +	if(!strcmp(option, "list")){ +		backend_config.list_devices = 0; +		if(!strcmp(value, "on")){ +			backend_config.list_devices = 1; +		} +		return 0; +	} +	else if(!strcmp(option, "detect")){ +		backend_config.detect = 0; +		if(!strcmp(value, "on")){ +			backend_config.detect = 1; +		} +		return 0; +	} + +	fprintf(stderr, "Unknown winmidi backend option %s\n", option); +	return 1; +} + +static int winmidi_configure_instance(instance* inst, char* option, char* value){ +	winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; +	if(!strcmp(option, "read")){ +		if(data->read){ +			fprintf(stderr, "winmidi instance %s already connected to an input device\n", inst->name); +			return 1; +		} +		data->read = strdup(value); +		return 0; +	} +	if(!strcmp(option, "write")){ +		if(data->write){ +			fprintf(stderr, "winmidi instance %s already connected to an output device\n", inst->name); +			return 1; +		} +		data->write = strdup(value); +		return 0; +	} + +	fprintf(stderr, "Unknown winmidi instance option %s\n", option); +	return 1; +} + +static instance* winmidi_instance(){ +	instance* i = mm_instance(); +	if(!i){ +		return NULL; +	} + +	i->impl = calloc(1, sizeof(winmidi_instance_data)); +	if(!i->impl){ +		fprintf(stderr, "Failed to allocate memory\n"); +		return NULL; +	} + +	return i; +} + +static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ +	char* next_token = NULL; +	winmidi_channel_ident ident = { +		.label = 0 +	}; + +	if(!strncmp(spec, "ch", 2)){ +		next_token = spec + 2; +		if(!strncmp(spec, "channel", 7)){ +			next_token = spec + 7; +		} +	} +	 +	if(!next_token){ +		fprintf(stderr, "Invalid winmidi channel specification %s\n", spec); +		return NULL; +	} + +	ident.fields.channel = strtoul(next_token, &next_token, 10); +	if(ident.fields.channel > 15){ +		fprintf(stderr, "MIDI channel out of range in winmidi channel spec %s\n", spec); +		return NULL; +	} + +	if(*next_token != '.'){ +		fprintf(stderr, "winmidi 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 winmidi channel control type in %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 winmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ +	winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; +	winmidi_channel_ident ident = { +		.label = 0 +	}; +	union { +		struct { +			uint8_t status; +			uint8_t data1; +			uint8_t data2; +			uint8_t unused; +		} components; +		DWORD dword; +	} output = { +		.dword = 0 +	}; +	size_t u; + +	//early exit +	if(!num){ +		return 0; +	} + +	if(!data->device_out){ +		fprintf(stderr, "winmidi instance %s has no output device\n", inst->name); +		return 0; +	} + +	for(u = 0; u < num; u++){ +		ident.label = c[u]->ident; + +		//build output message +		output.components.status = ident.fields.type | ident.fields.channel; +		output.components.data1 = ident.fields.control; +		output.components.data2 = v[u].normalised * 127.0; +		if(ident.fields.type == pitchbend){ +			output.components.data1 = ((int)(v[u].normalised * 16384.0)) & 0x7F; +			output.components.data2 = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; +		} +		else if(ident.fields.type == aftertouch){ +			output.components.data1 = v[u].normalised * 127.0; +			output.components.data2 = 0; +		} + +		midiOutShortMsg(data->device_out, output.dword); +	} +	 +	return 0; +} + +static char* winmidi_type_name(uint8_t typecode){ +	switch(typecode){ +		case note: +			return "note"; +		case cc: +			return "cc"; +		case pressure: +			return "pressure"; +		case aftertouch: +			return "aftertouch"; +		case pitchbend: +			return "pitch"; +	} +	return "unknown"; +} + +static int winmidi_handle(size_t num, managed_fd* fds){ +	size_t u; +	ssize_t bytes = 0; +	char recv_buf[1024]; +	channel* chan = NULL; +	if(!num){ +		return 0; +	} + +	//flush the feedback socket +	for(u = 0; u < num; u++){ +		bytes += recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); +	} + +	//push queued events +	EnterCriticalSection(&backend_config.push_events); +	for(u = 0; u < backend_config.events_active; u++){ +		if(backend_config.detect){ +			//pretty-print channel-wide events +			if(backend_config.event[u].channel.fields.type == pitchbend +					|| backend_config.event[u].channel.fields.type == aftertouch){ +				fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s, value %f\n", +						backend_config.event[u].inst->name, +						backend_config.event[u].channel.fields.channel, +						winmidi_type_name(backend_config.event[u].channel.fields.type), +						backend_config.event[u].value); +			} +			else{ +				fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d, value %f\n", +						backend_config.event[u].inst->name, +						backend_config.event[u].channel.fields.channel, +						winmidi_type_name(backend_config.event[u].channel.fields.type), +						backend_config.event[u].channel.fields.control, +						backend_config.event[u].value); +			} +		} +		chan = mm_channel(backend_config.event[u].inst, backend_config.event[u].channel.label, 0); +		if(chan){ +			mm_channel_event(chan, backend_config.event[u].value); +		} +	} +	DBGPF("winmidi flushed %" PRIsize_t " wakeups, handled %" PRIsize_t " events\n", bytes, backend_config.events_active); +	backend_config.events_active = 0; +	LeaveCriticalSection(&backend_config.push_events); +	return 0; +} + +static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DWORD_PTR inst, DWORD param1, DWORD param2){ +	winmidi_channel_ident ident = { +		.label = 0 +	}; +	channel_value val; +	union { +		struct { +			uint8_t status; +			uint8_t data1; +			uint8_t data2; +			uint8_t unused; +		} components; +		DWORD dword; +	} input = { +		.dword = 0 +	}; + +	//callbacks may run on different threads, so we queue all events and alert the main thread via the feedback socket +	DBGPF("winmidi input callback on thread %ld\n", GetCurrentThreadId()); + +	switch(message){ +		case MIM_MOREDATA: +			//processing too slow, do not immediately alert the main loop +		case MIM_DATA: +			//param1 has the message +			input.dword = param1; +			ident.fields.channel = input.components.status & 0x0F; +			ident.fields.type = input.components.status & 0xF0; +			ident.fields.control = input.components.data1; +			val.normalised = (double) input.components.data2 / 127.0; +			 +			if(ident.fields.type == 0x80){ +				ident.fields.type = note; +				val.normalised = 0; +			} +			else if(ident.fields.type == pitchbend){ +				ident.fields.control = 0; +				val.normalised = (double)((input.components.data2 << 7) | input.components.data1) / 16384.0; +			} +			else if(ident.fields.type == aftertouch){ +				ident.fields.control = 0; +				val.normalised = (double) input.components.data1 / 127.0; +			} +			break; +		case MIM_LONGDATA: +			//sysex message, ignore +			return; +		case MIM_ERROR: +			//error in input stream +			fprintf(stderr, "winmidi warning: error in input stream\n"); +			return; +		case MIM_OPEN: +		case MIM_CLOSE: +			//device opened/closed +			return; +		 +	} + +	DBGPF("winmidi incoming message type %d channel %d control %d value %f\n", +			ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised); + +	EnterCriticalSection(&backend_config.push_events); +	if(backend_config.events_alloc <= backend_config.events_active){ +		backend_config.event = realloc((void*) backend_config.event, (backend_config.events_alloc + 1) * sizeof(winmidi_event)); +		if(!backend_config.event){ +			fprintf(stderr, "Failed to allocate memory\n"); +			backend_config.events_alloc = 0; +			backend_config.events_active = 0; +			LeaveCriticalSection(&backend_config.push_events); +			return; +		} +		backend_config.events_alloc++; +	} +	backend_config.event[backend_config.events_active].inst = (instance*) inst; +	backend_config.event[backend_config.events_active].channel.label = ident.label; +	backend_config.event[backend_config.events_active].value = val; +	backend_config.events_active++; +	LeaveCriticalSection(&backend_config.push_events); + +	if(message != MIM_MOREDATA){ +		//alert the main loop +		send(backend_config.socket_pair[1], "w", 1, 0); +	} +} + +static void CALLBACK winmidi_output_callback(HMIDIOUT device, unsigned message, DWORD_PTR inst, DWORD param1, DWORD param2){ +	DBGPF("winmidi output callback on thread %ld\n", GetCurrentThreadId()); +} + +static int winmidi_match_input(char* prefix){ +	MIDIINCAPS input_caps; +	unsigned inputs = midiInGetNumDevs(); +	char* next_token = NULL; +	size_t n; + +	if(!prefix){ +		fprintf(stderr, "winmidi detected %u input devices\n", inputs); +	} +	else{ +		n = strtoul(prefix, &next_token, 10); +		if(!(*next_token) && n < inputs){ +			midiInGetDevCaps(n, &input_caps, sizeof(MIDIINCAPS)); +			fprintf(stderr, "winmidi selected input device %s for ID %d\n", input_caps.szPname, n); +			return n; +		} +	} + +	//find prefix match for input device +	for(n = 0; n < inputs; n++){ +		midiInGetDevCaps(n, &input_caps, sizeof(MIDIINCAPS)); +		if(!prefix){ +			printf("\tID %d: %s\n", n, input_caps.szPname); +		} +		else if(!strncmp(input_caps.szPname, prefix, strlen(prefix))){ +			fprintf(stderr, "winmidi selected input device %s (ID %" PRIsize_t ") for name %s\n", input_caps.szPname, n, prefix); +			return n; +		} +	} + +	return -1; +} + +static int winmidi_match_output(char* prefix){ +	MIDIOUTCAPS output_caps; +	unsigned outputs = midiOutGetNumDevs(); +	char* next_token = NULL; +	size_t n; + +	if(!prefix){ +		fprintf(stderr, "winmidi detected %u output devices\n", outputs); +	} +	else{ +		n = strtoul(prefix, &next_token, 10); +		if(!(*next_token) && n < outputs){ +			midiOutGetDevCaps(n, &output_caps, sizeof(MIDIOUTCAPS)); +			fprintf(stderr, "winmidi selected output device %s for ID %d\n", output_caps.szPname, n); +			return n; +		} +	} + +	//find prefix match for output device +	for(n = 0; n < outputs; n++){ +		midiOutGetDevCaps(n, &output_caps, sizeof(MIDIOUTCAPS)); +		if(!prefix){ +			printf("\tID %d: %s\n", n, output_caps.szPname); +		} +		else if(!strncmp(output_caps.szPname, prefix, strlen(prefix))){ +			fprintf(stderr, "winmidi selected output device %s (ID %" PRIsize_t " for name %s\n", output_caps.szPname, n, prefix); +			return n; +		} +	} + +	return -1; +} + +static int winmidi_start(){ +	size_t n = 0, p; +	int device, rv = -1; +	instance** inst = NULL; +	winmidi_instance_data* data = NULL; +	struct sockaddr_storage sockadd = { +		0 +	}; +	//this really should be a size_t but getsockname specifies int* for some reason +	int sockadd_len = sizeof(sockadd); +	char* error = NULL; +	DBGPF("winmidi main thread ID is %ld\n", GetCurrentThreadId()); + +	//fetch all instances +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		return 1; +	} + +	//no instances, we're done +	if(!n){ +		free(inst); +		return 0; +	} + +	//output device list if requested +	if(backend_config.list_devices){ +		winmidi_match_input(NULL); +		winmidi_match_output(NULL); +	} + +	//open the feedback sockets +	//for some reason the feedback connection fails to work on 'real' windows with ipv6 +	backend_config.socket_pair[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); +	if(backend_config.socket_pair[0] < 0){ +		fprintf(stderr, "winmidi failed to open feedback socket\n"); +		return 1; +	} +	if(getsockname(backend_config.socket_pair[0], (struct sockaddr*) &sockadd, &sockadd_len)){ +		FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, +			NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); +		fprintf(stderr, "winmidi failed to query feedback socket information: %s\n", error); +		LocalFree(error); +		return 1; +	} +	//getsockname on 'real' windows may not set the address - works on wine, though +	switch(sockadd.ss_family){ +		case AF_INET: +		case AF_INET6: +			((struct sockaddr_in*) &sockadd)->sin_family = AF_INET; +			((struct sockaddr_in*) &sockadd)->sin_addr.s_addr = htobe32(INADDR_LOOPBACK); +			break; +		//for some absurd reason 'real' windows announces the socket as AF_INET6 but rejects any connection unless its AF_INET +//		case AF_INET6: +//			((struct sockaddr_in6*) &sockadd)->sin6_addr = in6addr_any; +//			break; +		default: +			fprintf(stderr, "winmidi invalid feedback socket family\n"); +			return 1; +	} +	DBGPF("winmidi feedback socket family %d port %d\n", sockadd.ss_family, be16toh(((struct sockaddr_in*)&sockadd)->sin_port)); +	backend_config.socket_pair[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP); +	if(backend_config.socket_pair[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){ +		FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, +			NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); +		fprintf(stderr, "winmidi failed to connect to feedback socket: %s\n", error); +		LocalFree(error); +		return 1; +	} + +	//set up instances and start input +	for(p = 0; p < n; p++){ +		data = (winmidi_instance_data*) inst[p]->impl; +		inst[p]->ident = p; + +		//connect input device if requested +		if(data->read){ +			device = winmidi_match_input(data->read); +			if(device < 0){ +				fprintf(stderr, "Failed to match input device %s for instance %s\n", data->read, inst[p]->name); +				goto bail; +			} +			if(midiInOpen(&(data->device_in), device, (DWORD_PTR) winmidi_input_callback, (DWORD_PTR) inst[p], CALLBACK_FUNCTION | MIDI_IO_STATUS) != MMSYSERR_NOERROR){ +				fprintf(stderr, "Failed to open input device for instance %s\n", inst[p]->name); +				goto bail; +			} +			//start midi input callbacks +			midiInStart(data->device_in); +		} + +		//connect output device if requested +		if(data->write){ +			device = winmidi_match_output(data->write); +			if(device < 0){ +				fprintf(stderr, "Failed to match output device %s for instance %s\n", data->read, inst[p]->name); +				goto bail; +			} +			if(midiOutOpen(&(data->device_out), device, (DWORD_PTR) winmidi_output_callback, (DWORD_PTR) inst[p], CALLBACK_FUNCTION) != MMSYSERR_NOERROR){ +				fprintf(stderr, "Failed to open output device for instance %s\n", inst[p]->name); +				goto bail; +			} +		} +	} + +	//register the feedback socket to the core +	fprintf(stderr, "winmidi backend registering 1 descriptor to core\n"); +	if(mm_manage_fd(backend_config.socket_pair[0], BACKEND_NAME, 1, NULL)){ +		goto bail; +	} + +	rv = 0; +bail: +	free(inst); +	return rv; +} + +static int winmidi_shutdown(){ +	size_t n, u; +	instance** inst = NULL; +	winmidi_instance_data* data = NULL; + +	if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ +		fprintf(stderr, "Failed to fetch instance list\n"); +		return 1; +	} + +	for(u = 0; u < n; u++){ +		data = (winmidi_instance_data*) inst[u]->impl; +		free(data->read); +		data->read = NULL; +		free(data->write); +		data->write = NULL; + +		if(data->device_in){ +			midiInStop(data->device_in); +			midiInClose(data->device_in); +			data->device_in = NULL; +		} + +		if(data->device_out){ +			midiOutReset(data->device_out); +			midiOutClose(data->device_out); +			data->device_out = NULL; +		} +	} + +	free(inst); +	closesocket(backend_config.socket_pair[0]); +	closesocket(backend_config.socket_pair[1]); + +	EnterCriticalSection(&backend_config.push_events); +	free((void*) backend_config.event); +	backend_config.event = NULL; +	backend_config.events_alloc = 0; +	backend_config.events_active = 0; +	LeaveCriticalSection(&backend_config.push_events); +	DeleteCriticalSection(&backend_config.push_events); + +	fprintf(stderr, "winmidi backend shut down\n"); +	return 0; +} diff --git a/backends/winmidi.h b/backends/winmidi.h new file mode 100644 index 0000000..985c46a --- /dev/null +++ b/backends/winmidi.h @@ -0,0 +1,43 @@ +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static int winmidi_configure(char* option, char* value); +static int winmidi_configure_instance(instance* inst, char* option, char* value); +static instance* winmidi_instance(); +static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags); +static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v); +static int winmidi_handle(size_t num, managed_fd* fds); +static int winmidi_start(); +static int winmidi_shutdown(); + +typedef struct /*_winmidi_instance_data*/ { +	char* read; +	char* write; +	HMIDIIN device_in; +	HMIDIOUT device_out; +} winmidi_instance_data; + +enum /*_winmidi_channel_type*/ { +	none = 0, +	note = 0x90, +	cc = 0xB0, +	pressure = 0xA0, +	aftertouch = 0xD0, +	pitchbend = 0xE0 +}; + +typedef union { +	struct { +		uint8_t pad[5]; +		uint8_t type; +		uint8_t channel; +		uint8_t control; +	} fields; +	uint64_t label; +} winmidi_channel_ident; + +typedef struct /*_winmidi_event_queue_entry*/ { +	instance* inst; +	winmidi_channel_ident channel; +	channel_value value; +} winmidi_event; diff --git a/backends/winmidi.md b/backends/winmidi.md new file mode 100644 index 0000000..25a6378 --- /dev/null +++ b/backends/winmidi.md @@ -0,0 +1,60 @@ +### The `winmidi` backend + +This backend provides read-write access to the MIDI protocol via the Windows Multimedia API. + +It is only available when building for Windows. Care has been taken to keep the configuration +syntax similar to the `midi` backend, but due to differences in the internal programming interfaces, +some deviations may still be present. + +#### Global configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `list`	| `on`                  | `off`                 | List available input/output devices on startup | +| `detect`      | `on`                  | `off`                 | Output channel specifications for any events coming in on configured instances to help with configuration. | + +#### Instance configuration + +| Option	| Example value		| Default value 	| Description		| +|---------------|-----------------------|-----------------------|-----------------------| +| `read`	| `2`			| none			| MIDI device to connect for input | +| `write`	| `DeviceName`		| none			| MIDI device to connect for output | + +Input/output device names may either be prefixes of MIDI device names or numeric indices corresponding +to the listing shown at startup when using the global `list` option. + +#### Channel specification + +The `winmidi` backend supports mapping different MIDI events as 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: +``` +midi1.ch0.note9 > midi2.channel1.cc4 +midi1.channel15.pressure1 > midi1.channel0.note0 +midi1.ch1.aftertouch > midi2.ch2.cc0 +midi1.ch0.pitch > midi2.ch1.pitch +``` + +#### Known bugs / problems + +Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are +generated, which amount to the same thing according to the spec). This may be implemented as +a configuration option at a later time. + +As this is a Windows-only backend, testing may not be as frequent or thorough as for the Linux / multiplatform +backends. | 
