aboutsummaryrefslogtreecommitdiffhomepage
path: root/backends
diff options
context:
space:
mode:
Diffstat (limited to 'backends')
-rw-r--r--backends/Makefile55
-rw-r--r--backends/artnet.c143
-rw-r--r--backends/artnet.h2
-rw-r--r--backends/artnet.md8
-rw-r--r--backends/evdev.c37
-rw-r--r--backends/evdev.md3
-rw-r--r--backends/jack.c174
-rw-r--r--backends/jack.h22
-rw-r--r--backends/jack.md17
-rw-r--r--backends/libmmbackend.c8
-rw-r--r--backends/lua.c50
-rw-r--r--backends/lua.md5
-rw-r--r--backends/maweb.c114
-rw-r--r--backends/maweb.h2
-rw-r--r--backends/maweb.md2
-rw-r--r--backends/midi.c220
-rw-r--r--backends/midi.h14
-rw-r--r--backends/midi.md25
-rw-r--r--backends/mqtt.c1006
-rw-r--r--backends/mqtt.h87
-rw-r--r--backends/mqtt.md85
-rw-r--r--backends/openpixelcontrol.md4
-rw-r--r--backends/osc.c3
-rw-r--r--backends/osc.md2
-rw-r--r--backends/python.c36
-rw-r--r--backends/python.md5
-rw-r--r--backends/rtpmidi.c214
-rw-r--r--backends/rtpmidi.h23
-rw-r--r--backends/rtpmidi.md17
-rw-r--r--backends/sacn.c91
-rw-r--r--backends/sacn.h1
-rw-r--r--backends/sacn.md5
-rw-r--r--backends/visca.c466
-rw-r--r--backends/visca.h94
-rw-r--r--backends/visca.md70
-rw-r--r--backends/wininput.c766
-rw-r--r--backends/wininput.h54
-rw-r--r--backends/wininput.md135
-rw-r--r--backends/winmidi.c218
-rw-r--r--backends/winmidi.h22
-rw-r--r--backends/winmidi.md25
41 files changed, 4024 insertions, 306 deletions
diff --git a/backends/Makefile b/backends/Makefile
index 700c9b3..be870d6 100644
--- a/backends/Makefile
+++ b/backends/Makefile
@@ -1,16 +1,26 @@
.PHONY: all clean full
+# Backends that can only be built on Linux
LINUX_BACKENDS = midi.so evdev.so
-WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll
-BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.so
+# Backends that can only be built on Windows (mostly due to the .DLL extension)
+WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll wininput.dll visca.dll mqtt.dll
+# Backends that can be built on any platform that can load .SO libraries
+BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.so visca.so mqtt.so
+# Backends that require huge dependencies to be installed
OPTIONAL_BACKENDS = ola.so
+# Backends that need to be built manually (but still should be included in the clean target)
+MANUAL_BACKENDS = lua.dll
+
+# The backend library, providing platform-independent abstractions for common things
BACKEND_LIB = libmmbackend.o
+# Evaluate which system we are on
SYSTEM := $(shell uname -s)
# Generate debug symbols unless overridden
CFLAGS ?= -g
CPPFLAGS ?= -g
+# All backends are shared libraries
CFLAGS += -fPIC -I../ -Wall -Wpedantic
CPPFLAGS += -fPIC -I../
LDFLAGS += -shared
@@ -24,6 +34,7 @@ ifeq ($(SYSTEM),Darwin)
LDFLAGS += -undefined dynamic_lookup
endif
+# Most of these next few backends just pull in the backend lib, some set additional flags
artnet.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
artnet.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
artnet.dll: LDLIBS += -lws2_32
@@ -36,12 +47,20 @@ sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
sacn.dll: LDLIBS += -lws2_32
+visca.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
+visca.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
+visca.dll: LDLIBS += -lws2_32
+
+mqtt.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
+mqtt.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
+mqtt.dll: LDLIBS += -lws2_32
+
openpixelcontrol.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
openpixelcontrol.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
openpixelcontrol.dll: LDLIBS += -lws2_32
maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB)
-maweb.so: LDLIBS = -lssl
+maweb.so: LDLIBS = $(shell pkg-config --libs openssl || echo "-DBUILD_ERROR=\"Missing pkg-config data for openssl\"")
maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
maweb.dll: LDLIBS += -lws2_32
maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL
@@ -53,6 +72,8 @@ rtpmidi.dll: LDLIBS += -lws2_32 -liphlpapi
winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB)
winmidi.dll: LDLIBS += -lwinmm -lws2_32
+wininput.dll: LDLIBS += -lwinmm
+
jack.so: LDLIBS = -ljack -lpthread
midi.so: LDLIBS = -lasound
evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev || echo "-DBUILD_ERROR=\"Missing pkg-config data for libevdev\"")
@@ -62,28 +83,38 @@ 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\"")
-lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"")
+lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || pkg-config --cflags lua || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"")
+lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || pkg-config --libs lua || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"")
+lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || pkg-config --cflags lua || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"")
lua.dll: LDLIBS += -L../ -llua53
-python.so: CFLAGS += $(shell pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"")
-python.so: CFLAGS += $(shell pkg-config --libs python3 || pkg-config --libs python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"")
+# Python seems to ship their own little python3-config tool instead of properly maintaining their pkg-config files.
+# This one also spams a good deal of unwanted flags into CFLAGS, so we use only --includes. On the other hand, the --libs
+# info from this one seems to include the actual interpreter library only on some systems, which makes it worse than useless.
+python.so: CFLAGS += $(shell python3-config --includes || pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"")
+python.so: LDLIBS += $(shell pkg-config --libs python3-embed || python3-config --libs || pkg-config --libs python3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"")
+python.so: LDFLAGS += $(shell python3-config --ldflags || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"")
+python.dll: CFLAGS += $(shell python3-config --includes || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"")
+python.dll: LDLIBS += -L../ -lpython3
+# Generic rules on how to build .SO/.DLL's from C and CPP sources
%.so :: %.c %.h $(BACKEND_LIB)
- $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS)
+ $(CC) $(CFLAGS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) $(LDLIBS)
%.dll :: %.c %.h $(BACKEND_LIB)
$(CC) $(CFLAGS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) $(LDLIBS)
%.so :: %.cpp %.h
- $(CXX) $(CPPFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS)
+ $(CXX) $(CPPFLAGS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) $(LDLIBS)
+# This is the actual first named target, and thus the default
all: $(BACKEND_LIB) $(BACKENDS)
+# Build an import lib for the windows build if it's not already there
../libmmapi.a:
$(MAKE) -C ../ midimonster.exe
+# Override a bunch of stuff for the windows target and it's DLL dependencies
%.dll: export CC = x86_64-w64-mingw32-gcc
%.dll: LDLIBS += -lmmapi
%.dll: LDFLAGS += -L../
@@ -92,7 +123,9 @@ windows: CFLAGS += -Wno-format -Wno-pointer-sign
windows: export CC = x86_64-w64-mingw32-gcc
windows: ../libmmapi.a $(BACKEND_LIB) $(WINDOWS_BACKENDS)
+# Optional target including the backends that require large dependencies
full: $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS)
+# Clean up all generated files
clean:
- $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) $(WINDOWS_BACKENDS)
+ $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) $(WINDOWS_BACKENDS) $(MANUAL_BACKENDS)
diff --git a/backends/artnet.c b/backends/artnet.c
index e07ea52..dae9ba3 100644
--- a/backends/artnet.c
+++ b/backends/artnet.c
@@ -9,14 +9,19 @@
#define MAX_FDS 255
-static uint32_t next_frame = 0;
-static uint8_t default_net = 0;
-static size_t artnet_fds = 0;
-static artnet_descriptor* artnet_fd = NULL;
+static struct {
+ uint32_t next_frame;
+ uint8_t default_net;
+ size_t fds;
+ artnet_descriptor* fd;
+ uint8_t detect;
+} global_cfg = {
+ 0
+};
static int artnet_listener(char* host, char* port){
int fd;
- if(artnet_fds >= MAX_FDS){
+ if(global_cfg.fds >= MAX_FDS){
LOG("Backend descriptor limit reached");
return -1;
}
@@ -27,18 +32,19 @@ static int artnet_listener(char* host, char* port){
}
//store fd
- artnet_fd = realloc(artnet_fd, (artnet_fds + 1) * sizeof(artnet_descriptor));
- if(!artnet_fd){
+ global_cfg.fd = realloc(global_cfg.fd, (global_cfg.fds + 1) * sizeof(artnet_descriptor));
+ if(!global_cfg.fd){
close(fd);
+ global_cfg.fds = 0;
LOG("Failed to allocate memory");
return -1;
}
- LOGPF("Interface %" PRIsize_t " bound to %s port %s", artnet_fds, host, port);
- artnet_fd[artnet_fds].fd = fd;
- artnet_fd[artnet_fds].output_instances = 0;
- artnet_fd[artnet_fds].output_instance = NULL;
- artnet_fds++;
+ LOGPF("Interface %" PRIsize_t " bound to %s port %s", global_cfg.fds, host, port);
+ global_cfg.fd[global_cfg.fds].fd = fd;
+ global_cfg.fd[global_cfg.fds].output_instances = 0;
+ global_cfg.fd[global_cfg.fds].output_instance = NULL;
+ global_cfg.fds++;
return 0;
}
@@ -70,8 +76,8 @@ MM_PLUGIN_API int init(){
}
static uint32_t artnet_interval(){
- if(next_frame){
- return next_frame;
+ if(global_cfg.next_frame){
+ return global_cfg.next_frame;
}
return ARTNET_KEEPALIVE_INTERVAL;
}
@@ -80,7 +86,7 @@ static int artnet_configure(char* option, char* value){
char* host = NULL, *port = NULL, *fd_opts = NULL;
if(!strcmp(option, "net")){
//configure default net
- default_net = strtoul(value, NULL, 0);
+ global_cfg.default_net = strtoul(value, NULL, 0);
return 0;
}
else if(!strcmp(option, "bind")){
@@ -97,6 +103,16 @@ static int artnet_configure(char* option, char* value){
}
return 0;
}
+ else if(!strcmp(option, "detect")){
+ global_cfg.detect = 0;
+ if(!strcmp(value, "on")){
+ global_cfg.detect = 1;
+ }
+ else if(!strcmp(value, "verbose")){
+ global_cfg.detect = 2;
+ }
+ return 0;
+ }
LOGPF("Unknown backend option %s", option);
return 1;
@@ -111,7 +127,7 @@ static int artnet_instance(instance* inst){
return 1;
}
- data->net = default_net;
+ data->net = global_cfg.default_net;
for(u = 0; u < sizeof(data->data.channel) / sizeof(channel); u++){
data->data.channel[u].ident = u;
data->data.channel[u].instance = inst;
@@ -136,7 +152,7 @@ static int artnet_configure_instance(instance* inst, char* option, char* value){
else if(!strcmp(option, "iface") || !strcmp(option, "interface")){
data->fd_index = strtoul(value, NULL, 0);
- if(data->fd_index >= artnet_fds){
+ if(data->fd_index >= global_cfg.fds){
LOGPF("Invalid interface configured for instance %s", inst->name);
return 1;
}
@@ -152,6 +168,10 @@ static int artnet_configure_instance(instance* inst, char* option, char* value){
return mmbackend_parse_sockaddr(host, port ? port : ARTNET_PORT, &data->dest_addr, &data->dest_len);
}
+ else if(!strcmp(option, "realtime")){
+ data->realtime = strtoul(value, NULL, 10);
+ return 0;
+ }
LOGPF("Unknown instance option %s for instance %s", option, inst->name);
return 1;
@@ -223,7 +243,7 @@ static int artnet_transmit(instance* inst, artnet_output_universe* output){
};
memcpy(frame.data, data->data.out, 512);
- if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){
+ if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){
#ifdef _WIN32
if(WSAGetLastError() != WSAEWOULDBLOCK){
#else
@@ -234,8 +254,8 @@ static int artnet_transmit(instance* inst, artnet_output_universe* output){
}
//reschedule frame output
output->mark = 1;
- if(!next_frame || next_frame > ARTNET_SYNTHESIZE_MARGIN){
- next_frame = ARTNET_SYNTHESIZE_MARGIN;
+ if(!global_cfg.next_frame || global_cfg.next_frame > ARTNET_SYNTHESIZE_MARGIN){
+ global_cfg.next_frame = ARTNET_SYNTHESIZE_MARGIN;
}
return 0;
}
@@ -278,23 +298,26 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v)
}
if(mark){
- //find last frame time
- for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){
- if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){
+ //find output control data for the instance
+ for(u = 0; u < global_cfg.fd[data->fd_index].output_instances; u++){
+ if(global_cfg.fd[data->fd_index].output_instance[u].label == inst->ident){
break;
}
}
- frame_delta = mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame;
- //check output rate limit, request next frame
- if(frame_delta < ARTNET_FRAME_TIMEOUT){
- artnet_fd[data->fd_index].output_instance[u].mark = 1;
- if(!next_frame || next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){
- next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta);
+ if(!data->realtime){
+ frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].output_instance[u].last_frame;
+
+ //check output rate limit, request next frame
+ if(frame_delta < ARTNET_FRAME_TIMEOUT){
+ global_cfg.fd[data->fd_index].output_instance[u].mark = 1;
+ if(!global_cfg.next_frame || global_cfg.next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){
+ global_cfg.next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta);
+ }
+ return 0;
}
- return 0;
}
- return artnet_transmit(inst, artnet_fd[data->fd_index].output_instance + u);
+ return artnet_transmit(inst, global_cfg.fd[data->fd_index].output_instance + u);
}
return 0;
@@ -307,6 +330,11 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){
channel_value val;
artnet_instance_data* data = (artnet_instance_data*) inst->impl;
+ if(!data->last_input && global_cfg.detect){
+ LOGPF("Valid data on instance %s (Net %d Universe %d): %d channels", inst->name, data->net, data->uni, be16toh(frame->length));
+ }
+ data->last_input = mm_timestamp();
+
if(be16toh(frame->length) > 512){
LOGPF("Invalid frame channel count: %d", be16toh(frame->length));
return 1;
@@ -366,23 +394,23 @@ static int artnet_handle(size_t num, managed_fd* fds){
artnet_pkt* frame = (artnet_pkt*) recv_buf;
//transmit keepalive & synthesized frames
- next_frame = 0;
- for(u = 0; u < artnet_fds; u++){
- for(c = 0; c < artnet_fd[u].output_instances; c++){
- synthesize_delta = timestamp - artnet_fd[u].output_instance[c].last_frame;
- if((artnet_fd[u].output_instance[c].mark
+ global_cfg.next_frame = 0;
+ for(u = 0; u < global_cfg.fds; u++){
+ for(c = 0; c < global_cfg.fd[u].output_instances; c++){
+ synthesize_delta = timestamp - global_cfg.fd[u].output_instance[c].last_frame;
+ if((global_cfg.fd[u].output_instance[c].mark
&& synthesize_delta >= ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN) //synthesize next frame
|| synthesize_delta >= ARTNET_KEEPALIVE_INTERVAL){ //keepalive timeout
- inst = mm_instance_find(BACKEND_NAME, artnet_fd[u].output_instance[c].label);
+ inst = mm_instance_find(BACKEND_NAME, global_cfg.fd[u].output_instance[c].label);
if(inst){
- artnet_transmit(inst, artnet_fd[u].output_instance + c);
+ artnet_transmit(inst, global_cfg.fd[u].output_instance + c);
}
}
//update next_frame
- if(artnet_fd[u].output_instance[c].mark
- && (!next_frame || next_frame > ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta)){
- next_frame = ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta;
+ if(global_cfg.fd[u].output_instance[c].mark
+ && (!global_cfg.next_frame || global_cfg.next_frame > ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta)){
+ global_cfg.next_frame = ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta;
}
}
}
@@ -400,6 +428,9 @@ static int artnet_handle(size_t num, managed_fd* fds){
if(inst && artnet_process_frame(inst, frame)){
LOG("Failed to process frame");
}
+ else if(!inst && global_cfg.detect > 1){
+ LOGPF("Received data for unconfigured universe %d (net %d) on descriptor %" PRIsize_t, frame->universe, frame->net, (((uint64_t) fds[u].impl) & 0xFF));
+ }
}
}
} while(bytes_read > 0);
@@ -429,7 +460,7 @@ static int artnet_start(size_t n, instance** inst){
.label = 0
};
- if(!artnet_fds){
+ if(!global_cfg.fds){
LOG("Failed to start backend: no descriptors bound");
return 1;
}
@@ -452,23 +483,23 @@ static int artnet_start(size_t n, instance** inst){
//if enabled for output, add to keepalive tracking
if(data->dest_len){
- artnet_fd[data->fd_index].output_instance = realloc(artnet_fd[data->fd_index].output_instance, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(artnet_output_universe));
+ global_cfg.fd[data->fd_index].output_instance = realloc(global_cfg.fd[data->fd_index].output_instance, (global_cfg.fd[data->fd_index].output_instances + 1) * sizeof(artnet_output_universe));
- if(!artnet_fd[data->fd_index].output_instance){
+ if(!global_cfg.fd[data->fd_index].output_instance){
LOG("Failed to allocate memory");
goto bail;
}
- artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].label = id.label;
- artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].last_frame = 0;
- artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].mark = 0;
+ global_cfg.fd[data->fd_index].output_instance[global_cfg.fd[data->fd_index].output_instances].label = id.label;
+ global_cfg.fd[data->fd_index].output_instance[global_cfg.fd[data->fd_index].output_instances].last_frame = 0;
+ global_cfg.fd[data->fd_index].output_instance[global_cfg.fd[data->fd_index].output_instances].mark = 0;
- artnet_fd[data->fd_index].output_instances++;
+ global_cfg.fd[data->fd_index].output_instances++;
}
}
- LOGPF("Registering %" PRIsize_t " descriptors to core", artnet_fds);
- for(u = 0; u < artnet_fds; u++){
- if(mm_manage_fd(artnet_fd[u].fd, BACKEND_NAME, 1, (void*) u)){
+ LOGPF("Registering %" PRIsize_t " descriptors to core", global_cfg.fds);
+ for(u = 0; u < global_cfg.fds; u++){
+ if(mm_manage_fd(global_cfg.fd[u].fd, BACKEND_NAME, 1, (void*) u)){
goto bail;
}
}
@@ -485,11 +516,13 @@ static int artnet_shutdown(size_t n, instance** inst){
free(inst[p]->impl);
}
- for(p = 0; p < artnet_fds; p++){
- close(artnet_fd[p].fd);
- free(artnet_fd[p].output_instance);
+ for(p = 0; p < global_cfg.fds; p++){
+ close(global_cfg.fd[p].fd);
+ free(global_cfg.fd[p].output_instance);
}
- free(artnet_fd);
+ free(global_cfg.fd);
+ global_cfg.fd = NULL;
+ global_cfg.fds = 0;
LOG("Backend shut down");
return 0;
diff --git a/backends/artnet.h b/backends/artnet.h
index a517aa0..b42646d 100644
--- a/backends/artnet.h
+++ b/backends/artnet.h
@@ -47,6 +47,8 @@ typedef struct /*_artnet_instance_model*/ {
socklen_t dest_len;
artnet_universe data;
size_t fd_index;
+ uint64_t last_input;
+ uint8_t realtime;
} artnet_instance_data;
typedef union /*_artnet_instance_id*/ {
diff --git a/backends/artnet.md b/backends/artnet.md
index 383203d..f035ad7 100644
--- a/backends/artnet.md
+++ b/backends/artnet.md
@@ -9,8 +9,9 @@ Art-Netâ„¢ Designed by and Copyright Artistic Licence Holdings Ltd.
| 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. |
+| `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 |
+| `detect` | `on`, `verbose` | `off` | Output additional information on received data packets to help with configuring complex scenarios |
#### Instance configuration
@@ -20,6 +21,7 @@ Art-Netâ„¢ Designed by and Copyright Artistic Licence Holdings Ltd.
| `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 |
+| `realtime` | `1` | `0` | Disable the recommended rate-limiting (approx. 44 packets per second) for this instance |
#### Channel specification
@@ -38,3 +40,7 @@ net1.1+2 > net2.5+123
A normal channel that is part of a wide channel can not be mapped individually.
#### Known bugs / problems
+
+When using this backend for output with a fast event source, some events may appear to be lost due to the packet output rate limiting
+mandated by the [ArtNet specification](https://artisticlicence.com/WebSiteMaster/User%20Guides/art-net.pdf) (Section `Refresh rate`).
+This limit can be disabled on a per-instance basis using the `realtime` instance option.
diff --git a/backends/evdev.c b/backends/evdev.c
index 8a14200..8f7c4f9 100644
--- a/backends/evdev.c
+++ b/backends/evdev.c
@@ -113,12 +113,14 @@ static int evdev_attach(instance* inst, evdev_instance_data* data, char* node){
static char* evdev_find(char* name){
int fd = -1;
struct dirent* file = NULL;
- char file_path[PATH_MAX * 2];
+ char file_path[PATH_MAX * 2], *result = calloc(PATH_MAX * 2, sizeof(char));
DIR* nodes = opendir(INPUT_NODES);
- char device_name[UINPUT_MAX_NAME_SIZE], *result = NULL;
+ char device_name[UINPUT_MAX_NAME_SIZE];
+ size_t min_distance = -1, distance = 0;
if(!nodes){
LOGPF("Failed to query input device nodes in %s: %s", INPUT_NODES, strerror(errno));
+ free(result);
return NULL;
}
@@ -141,20 +143,23 @@ static char* evdev_find(char* name){
close(fd);
if(!strncmp(device_name, name, strlen(name))){
- LOGPF("Matched name %s for %s: %s", device_name, name, file_path);
- break;
+ distance = strlen(device_name) - strlen(name);
+ LOGPF("Matched name %s as candidate (distance %" PRIsize_t ") for %s: %s", device_name, distance, name, file_path);
+ if(distance < min_distance){
+ strncpy(result, file_path, (PATH_MAX * 2) - 1);
+ min_distance = distance;
+ }
}
}
}
- if(file){
- result = calloc(strlen(file_path) + 1, sizeof(char));
- if(result){
- strncpy(result, file_path, strlen(file_path));
- }
- }
-
closedir(nodes);
+
+ if(!result[0]){
+ free(result);
+ return NULL;
+ }
+ LOGPF("Using %s for input name %s", result, name);
return result;
}
@@ -206,6 +211,7 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) {
else if(data->relative_axis[data->relative_axes].max == 0){
LOGPF("Relative axis configuration for %s.%s has invalid range", inst->name, option + 8);
}
+ //this does not crash on single-integer `value`s because strtoll sets `next_token` to the terminator
data->relative_axis[data->relative_axes].current = strtoul(next_token, NULL, 0);
if(data->relative_axis[data->relative_axes].code < 0){
LOGPF("Failed to configure relative axis extents for %s.%s", inst->name, option + 8);
@@ -366,7 +372,9 @@ static int evdev_handle(size_t num, managed_fd* fds){
data = (evdev_instance_data*) inst->impl;
- for(read_status = libevdev_next_event(data->input_ev, read_flags, &ev); read_status >= 0; read_status = libevdev_next_event(data->input_ev, read_flags, &ev)){
+ for(read_status = libevdev_next_event(data->input_ev, read_flags, &ev);
+ read_status == LIBEVDEV_READ_STATUS_SUCCESS || read_status == LIBEVDEV_READ_STATUS_SYNC;
+ read_status = libevdev_next_event(data->input_ev, read_flags, &ev)){
read_flags = LIBEVDEV_READ_FLAG_NORMAL;
if(read_status == LIBEVDEV_READ_STATUS_SYNC){
read_flags = LIBEVDEV_READ_FLAG_SYNC;
@@ -382,6 +390,11 @@ static int evdev_handle(size_t num, managed_fd* fds){
return 1;
}
}
+
+ if(read_status != -EAGAIN){
+ LOGPF("Failed to handle events: %s\n", strerror(-read_status));
+ return 1;
+ }
}
return 0;
diff --git a/backends/evdev.md b/backends/evdev.md
index d57201d..e7ba3cc 100644
--- a/backends/evdev.md
+++ b/backends/evdev.md
@@ -16,7 +16,7 @@ This functionality may require elevated privileges (such as special group member
| 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) |
+| `input` | `Xbox Wireless` | none | Presentation name of evdev device to use as input (most-specific prefix matched), can be used instead of the `device` option |
| `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 |
@@ -49,7 +49,6 @@ If relative axes are used without specifying their extents, the channel will gen
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
diff --git a/backends/jack.c b/backends/jack.c
index c84ed0f..fe74a80 100644
--- a/backends/jack.c
+++ b/backends/jack.c
@@ -18,8 +18,6 @@
#endif
#endif
-//FIXME pitchbend range is somewhat oob
-
static struct /*_mmjack_backend_cfg*/ {
unsigned verbosity;
volatile sig_atomic_t jack_shutdown;
@@ -80,13 +78,98 @@ static int mmjack_midiqueue_append(mmjack_port* port, mmjack_channel_ident ident
return 0;
}
+static void mmjack_process_midiout(void* buffer, size_t sample_offset, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){
+ jack_midi_data_t* event_data = jack_midi_event_reserve(buffer, sample_offset, (type == midi_aftertouch || type == midi_program) ? 2 : 3);
+
+ if(!event_data){
+ LOG("Failed to reserve MIDI stream data");
+ return;
+ }
+
+ //build midi event
+ event_data[0] = channel | type;
+ event_data[1] = control & 0x7F;
+ event_data[2] = value & 0x7F;
+
+ if(type == midi_pitchbend){
+ event_data[1] = value & 0x7F;
+ event_data[2] = (value >> 7) & 0x7F;
+ }
+ else if(type == midi_aftertouch || type == midi_program){
+ event_data[1] = value & 0x7F;
+ event_data[2] = 0;
+ }
+}
+
+//this state machine was copied more-or-less verbatim from the alsa midi implementation - fixes there will need to be integrated
+static void mmjack_handle_epn(mmjack_port* port, uint8_t chan, uint16_t control, uint16_t value){
+ mmjack_channel_ident ident = {
+ .label = 0
+ };
+
+ //switching between nrpn and rpn clears all valid bits
+ if(((port->epn_status[chan] & EPN_NRPN) && (control == 101 || control == 100))
+ || (!(port->epn_status[chan] & EPN_NRPN) && (control == 99 || control == 98))){
+ port->epn_status[chan] &= ~(EPN_NRPN | EPN_PARAMETER_LO | EPN_PARAMETER_HI);
+ }
+
+ //setting an address always invalidates the value valid bits
+ if(control >= 98 && control <= 101){
+ port->epn_status[chan] &= ~EPN_VALUE_HI;
+ }
+
+ //parameter hi
+ if(control == 101 || control == 99){
+ port->epn_control[chan] &= 0x7F;
+ port->epn_control[chan] |= value << 7;
+ port->epn_status[chan] |= EPN_PARAMETER_HI | ((control == 99) ? EPN_NRPN : 0);
+ if(control == 101 && value == 127){
+ port->epn_status[chan] &= ~EPN_PARAMETER_HI;
+ }
+ }
+
+ //parameter lo
+ if(control == 100 || control == 98){
+ port->epn_control[chan] &= ~0x7F;
+ port->epn_control[chan] |= value & 0x7F;
+ port->epn_status[chan] |= EPN_PARAMETER_LO | ((control == 98) ? EPN_NRPN : 0);
+ if(control == 100 && value == 127){
+ port->epn_status[chan] &= ~EPN_PARAMETER_LO;
+ }
+ }
+
+ //value hi, clears low, mark as update candidate
+ if(control == 6
+ //check if parameter is set before accepting value update
+ && ((port->epn_status[chan] & (EPN_PARAMETER_HI | EPN_PARAMETER_LO)) == (EPN_PARAMETER_HI | EPN_PARAMETER_LO))){
+ port->epn_value[chan] = value << 7;
+ port->epn_status[chan] |= EPN_VALUE_HI;
+ }
+
+ //value lo, flush the value
+ if(control == 38
+ && port->epn_status[chan] & EPN_VALUE_HI){
+ port->epn_value[chan] &= ~0x7F;
+ port->epn_value[chan] |= value & 0x7F;
+ port->epn_status[chan] &= ~EPN_VALUE_HI;
+
+ //find the updated channel
+ ident.fields.sub_type = port->epn_status[chan] & EPN_NRPN ? midi_nrpn : midi_rpn;
+ ident.fields.sub_channel = chan;
+ ident.fields.sub_control = port->epn_control[chan];
+
+ //ident.fields.port set on output in mmjack_handle_midi
+ mmjack_midiqueue_append(port, ident, port->epn_value[chan]);
+ }
+}
+
static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){
+ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl;
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;
+ size_t u, frame;
uint16_t value;
if(port->input){
@@ -109,10 +192,19 @@ static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes
ident.fields.sub_control = 0;
value = event.buffer[1] | (event.buffer[2] << 7);
}
- else if(ident.fields.sub_type == midi_aftertouch){
+ else if(ident.fields.sub_type == midi_aftertouch || ident.fields.sub_type == midi_program){
ident.fields.sub_control = 0;
value = event.buffer[1];
}
+
+ //forward the EPN CCs to the EPN state machine
+ if(ident.fields.sub_type == midi_cc
+ && ((ident.fields.sub_control <= 101 && ident.fields.sub_control >= 98)
+ || ident.fields.sub_control == 6
+ || ident.fields.sub_control == 38)){
+ mmjack_handle_epn(port, ident.fields.sub_channel, ident.fields.sub_control, value);
+ }
+
//append midi data
mmjack_midiqueue_append(port, ident, value);
}
@@ -124,30 +216,33 @@ static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes
//clear buffer
jack_midi_clear_buffer(buffer);
+ frame = 0;
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){
- LOG("Failed to reserve MIDI stream data");
- 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;
+
+ if(ident.fields.sub_type == midi_rpn
+ || ident.fields.sub_type == midi_nrpn){
+ //transmit parameter number
+ mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, (ident.fields.sub_type == midi_rpn) ? 101 : 99, (ident.fields.sub_control >> 7) & 0x7F);
+ mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, (ident.fields.sub_type == midi_rpn) ? 100 : 98, ident.fields.sub_control & 0x7F);
+
+ //transmit parameter value
+ mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 6, (port->queue[u].raw >> 7) & 0x7F);
+ mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 38, port->queue[u].raw & 0x7F);
+
+ if(!data->midi_epn_tx_short){
+ //clear active parameter
+ mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 101, 127);
+ mmjack_process_midiout(buffer, frame++, midi_cc, ident.fields.sub_channel, 100, 127);
+ }
}
else{
- event_data[1] = ident.fields.sub_control;
- event_data[2] = port->queue[u].raw & 0x7F;
+ mmjack_process_midiout(buffer, frame++, ident.fields.sub_type, ident.fields.sub_channel, ident.fields.sub_control, port->queue[u].raw);
}
}
- if(port->queue_len){
- DBGPF("Wrote %" PRIsize_t " MIDI events to port %s", port->queue_len, port->name);
+ if(frame){
+ DBGPF("Wrote %" PRIsize_t " MIDI events to port %s", frame, port->name);
}
port->queue_len = 0;
}
@@ -305,6 +400,13 @@ static int mmjack_configure_instance(instance* inst, char* option, char* value){
data->server_name = strdup(value);
return 0;
}
+ else if(!strcmp(option, "epn-tx")){
+ data->midi_epn_tx_short = 0;
+ if(!strcmp(value, "short")){
+ data->midi_epn_tx_short = 1;
+ }
+ return 0;
+ }
//register new port, first check for unique name
for(p = 0; p < data->ports; p++){
@@ -385,12 +487,23 @@ static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){
ident->fields.sub_type = midi_pressure;
next_token += 8;
}
+ else if(!strncmp(next_token, "rpn", 3)){
+ ident->fields.sub_type = midi_rpn;
+ next_token += 3;
+ }
+ else if(!strncmp(next_token, "nrpn", 4)){
+ ident->fields.sub_type = midi_nrpn;
+ next_token += 4;
+ }
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 if(!strncmp(next_token, "program", 7)){
+ ident->fields.sub_type = midi_program;
+ }
else{
LOGPF("Unknown MIDI control type in spec %s", spec);
return 1;
@@ -399,7 +512,9 @@ static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){
ident->fields.sub_control = strtoul(next_token, NULL, 10);
if(ident->fields.sub_type == midi_none
- || ident->fields.sub_control > 127){
+ || (ident->fields.sub_type != midi_nrpn
+ && ident->fields.sub_type != midi_rpn
+ && ident->fields.sub_control > 127)){
LOGPF("Invalid MIDI spec %s", spec);
return 1;
}
@@ -467,9 +582,12 @@ static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v)
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(ident.fields.sub_type == midi_pitchbend
+ || ident.fields.sub_type == midi_nrpn
+ || ident.fields.sub_type == midi_rpn){
+ value = ((uint16_t)(v[u].normalised * 16383.0));
}
+
if(mmjack_midiqueue_append(data->port + ident.fields.port, ident, value)){
pthread_mutex_unlock(&data->port[ident.fields.port].lock);
return 1;
@@ -494,8 +612,10 @@ static void mmjack_handle_midi(instance* inst, size_t index, mmjack_port* port){
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;
+ if(port->queue[u].ident.fields.sub_type == midi_pitchbend
+ || port->queue[u].ident.fields.sub_type == midi_rpn
+ || port->queue[u].ident.fields.sub_type == midi_nrpn){
+ val.normalised = ((double)port->queue[u].raw) / 16383.0;
}
else{
val.normalised = ((double)port->queue[u].raw) / 127.0;
diff --git a/backends/jack.h b/backends/jack.h
index 03ce052..42905f1 100644
--- a/backends/jack.h
+++ b/backends/jack.h
@@ -16,22 +16,29 @@ static int mmjack_shutdown(size_t n, instance** inst);
#define JACK_DEFAULT_SERVER_NAME "default"
#define JACK_MIDIQUEUE_CHUNK 10
+#define EPN_NRPN 8
+#define EPN_PARAMETER_HI 4
+#define EPN_PARAMETER_LO 2
+#define EPN_VALUE_HI 1
+
enum /*mmjack_midi_channel_type*/ {
midi_none = 0,
midi_note = 0x90,
- midi_cc = 0xB0,
midi_pressure = 0xA0,
+ midi_cc = 0xB0,
+ midi_program = 0xC0,
midi_aftertouch = 0xD0,
- midi_pitchbend = 0xE0
+ midi_pitchbend = 0xE0,
+ midi_rpn = 0xF1,
+ midi_nrpn = 0xF2
};
typedef union {
struct {
uint32_t port;
- uint8_t pad;
uint8_t sub_type;
uint8_t sub_channel;
- uint8_t sub_control;
+ uint16_t sub_control;
} fields;
uint64_t label;
} mmjack_channel_ident;
@@ -58,10 +65,15 @@ typedef struct /*_mmjack_port_data*/ {
double min;
uint8_t mark;
double last;
+
size_t queue_len;
size_t queue_alloc;
mmjack_midiqueue* queue;
+ uint16_t epn_control[16];
+ uint16_t epn_value[16];
+ uint8_t epn_status[16];
+
pthread_mutex_t lock;
} mmjack_port;
@@ -70,6 +82,8 @@ typedef struct /*_jack_instance_data*/ {
char* client_name;
int fd;
+ uint8_t midi_epn_tx_short;
+
jack_client_t* client;
size_t ports;
mmjack_port* port;
diff --git a/backends/jack.md b/backends/jack.md
index b6ff5a9..c67f060 100644
--- a/backends/jack.md
+++ b/backends/jack.md
@@ -16,6 +16,7 @@ transport of control data via either JACK midi ports or control voltage (CV) inp
|---------------|-----------------------|-----------------------|-----------------------|
| `name` | `Controller` | `MIDIMonster` | Client name for the JACK connection |
| `server` | `jackserver` | `default` | JACK server identifier to connect to |
+| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting a MIDI `nrpn` or `rpn` parameter. |
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:
@@ -55,6 +56,9 @@ MIDI ports provide subchannels for the various MIDI controls available. Each MID
corresponding pressure controls for each note, 128 control change (CC) controls (numbered likewise),
one channel wide "aftertouch" control and one channel-wide pitchbend control.
+Every MIDI channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using
+the CC controls 101/100/99/98/38/6. Both control types have 14-bit IDs and 14-bit values.
+
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).
@@ -65,13 +69,18 @@ The following values are recognized for `type`:
* `pressure` - Note pressure/aftertouch messages
* `aftertouch` - Channel-wide aftertouch messages
* `pitch` - Channel pitchbend messages
+* `program` - Channel program change messages
+* `rpn` - Registered parameter numbers (14-bit extension)
+* `nrpn` - Non-registered parameter numbers (14-bit extension)
-The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`.
+The `pitch`, `aftertouch` and `program` messages/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
+jack2.midi_in.ch0.nrpn900 > jack1.midi_out.ch1.rpn1
+jack1.midi_in.ch15.note1 > jack1.midi_out.ch4.program
```
The MIDI subchannel syntax is intentionally kept compatible to the different MIDI backends also supported
@@ -79,6 +88,12 @@ by the MIDIMonster
#### Known bugs / problems
+MIDI extended parameter numbers (EPNs, the `rpn` and `nrpn` control types) will also generate events on the controls (CC 101 through
+98, 38 and 6) that are used as the lower layer transport. When using EPNs, mapping those controls is probably not useful.
+
+EPN control types support only the full 14-bit transfer encoding, not the shorter variant transmitting only the 7
+high-order bits. This may be changed if there is sufficient interest in the functionality.
+
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
index bad048c..18611e1 100644
--- a/backends/libmmbackend.c
+++ b/backends/libmmbackend.c
@@ -3,6 +3,10 @@
#define LOGPF(format, ...) fprintf(stderr, "libmmbe\t" format "\n", __VA_ARGS__)
#define LOG(message) fprintf(stderr, "libmmbe\t%s\n", (message))
+#ifndef _WIN32
+ #define closesocket close
+#endif
+
int mmbackend_strdup(char** dest, char* src){
if(*dest){
free(*dest);
@@ -186,14 +190,14 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin
if(listener){
status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen);
if(status < 0){
- close(fd);
+ closesocket(fd);
continue;
}
}
else{
status = connect(fd, addr_it->ai_addr, addr_it->ai_addrlen);
if(status < 0){
- close(fd);
+ closesocket(fd);
continue;
}
}
diff --git a/backends/lua.c b/backends/lua.c
index 98ce369..0a638f7 100644
--- a/backends/lua.c
+++ b/backends/lua.c
@@ -137,6 +137,8 @@ static int lua_update_timerfd(){
}
static void lua_thread_resume(size_t current_thread){
+ int thread_status = 0;
+
//push coroutine reference
lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD);
lua_pushnumber(thread[current_thread].thread, current_thread);
@@ -144,9 +146,23 @@ static void lua_thread_resume(size_t current_thread){
//call thread main
DBGPF("Resuming thread %" PRIsize_t " on %s", current_thread, thread[current_thread].instance->name);
- if(lua_resume(thread[current_thread].thread, NULL, 0) != LUA_YIELD){
- DBGPF("Thread %" PRIsize_t " on %s terminated", current_thread, thread[current_thread].instance->name);
+ //the lua_resume API has changed with lua5.4
+ #if LUA_VERSION_NUM > 503
+ int results = 0;
+ thread_status = lua_resume(thread[current_thread].thread, NULL, 0, &results);
+ #else
+ thread_status = lua_resume(thread[current_thread].thread, NULL, 0);
+ #endif
+
+ if(thread_status == LUA_YIELD){
+ DBGPF("Thread %" PRIsize_t " on %s yielded execution", current_thread, thread[current_thread].instance->name);
+ }
+ else{
thread[current_thread].timeout = 0;
+ LOGPF("Thread %" PRIsize_t " on %s terminated", current_thread, thread[current_thread].instance->name);
+ if(thread_status){
+ LOGPF("Last error message: %s", lua_tostring(thread[current_thread].thread, -1));
+ }
}
//remove coroutine reference
@@ -166,6 +182,30 @@ static instance* lua_fetch_instance(lua_State* interpreter){
return inst;
}
+static int lua_callback_channels(lua_State* interpreter){
+ size_t u;
+ instance* inst = lua_fetch_instance(interpreter);
+ lua_instance_data* data = (lua_instance_data*) inst->impl;
+
+ if(!last_timestamp){
+ LOG("The channels() API will not return usable results before the configuration has been read completely");
+ }
+
+ //create a table for the return array
+ lua_createtable(interpreter, data->channels, 0);
+
+ for(u = 0; u < data->channels; u++){
+ //push the key
+ lua_pushnumber(interpreter, u + 1);
+ //push the value
+ lua_pushstring(interpreter, data->channel[u].name);
+ //settable pops key and value, leaving the table
+ lua_settable(interpreter, -3);
+ }
+
+ return 1;
+}
+
static int lua_callback_thread(lua_State* interpreter){
instance* inst = lua_fetch_instance(interpreter);
size_t u = threads;
@@ -467,6 +507,7 @@ static int lua_instance(instance* inst){
lua_register(data->interpreter, "thread", lua_callback_thread);
lua_register(data->interpreter, "sleep", lua_callback_sleep);
lua_register(data->interpreter, "cleanup_handler", lua_callback_cleanup_handler);
+ lua_register(data->interpreter, "channels", lua_callback_channels);
//store instance pointer to the lua state
lua_pushstring(data->interpreter, LUA_REGISTRY_KEY);
@@ -604,6 +645,7 @@ static int lua_resolve_symbol(lua_State* interpreter, char* symbol){
|| !strcmp(symbol, "input_channel")
|| !strcmp(symbol, "timestamp")
|| !strcmp(symbol, "cleanup_handler")
+ || !strcmp(symbol, "channels")
|| !strcmp(symbol, "interval")){
return LUA_NOREF;
}
@@ -622,6 +664,10 @@ static int lua_start(size_t n, instance** inst){
int default_handler;
channel_value v;
+ #ifdef LUA_VERSION_NUM
+ DBGPF("Lua backend built with %s (%d)", LUA_VERSION, LUA_VERSION_NUM);
+ #endif
+
//resolve channels to their handler functions
for(u = 0; u < n; u++){
data = (lua_instance_data*) inst[u]->impl;
diff --git a/backends/lua.md b/backends/lua.md
index b2f40e0..026c945 100644
--- a/backends/lua.md
+++ b/backends/lua.md
@@ -6,8 +6,8 @@ and manipulate events using the Lua scripting language.
Every instance has its own interpreter state which can be loaded with custom 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. Alternatively, a designated default channel handler
-which will receive events for all incoming channels may be supplied in the configuration.
+with the normalized event value (as a Lua `number` type) as parameter. Alternatively, a designated
+default channel handler which will receive events for all incoming channels may be set in the configuration.
The backend can also call Lua functions repeatedly using a timer, allowing users to implement time-based
functionality (such as evaluating a fixed mathematical function or outputting periodic updates).
@@ -25,6 +25,7 @@ The following functions are provided within the Lua interpreter for interaction
| `timestamp()` | `print(timestamp())` | Returns the core timestamp for this iteration with millisecond resolution. This is not a performance timer, but intended for timeouting, etc |
| `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) |
| `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds |
+| `channels()` | `chans = channels()` | Fetch an array of all currently known channels on the instance. Note that this function only works properly after the configuration has been read completely, i.e. any time after startup |
While a channel handler executes, calling `input_value` for that channel returns the previous value.
The stored value is updated once the handler returns.
diff --git a/backends/maweb.c b/backends/maweb.c
index 97d4cea..8b878b0 100644
--- a/backends/maweb.c
+++ b/backends/maweb.c
@@ -1,4 +1,5 @@
#define BACKEND_NAME "maweb"
+//#define DEBUG
#include <string.h>
#include <unistd.h>
@@ -15,14 +16,11 @@
#define WS_FLAG_FIN 0x80
#define WS_FLAG_MASK 0x80
-/*
- * TODO handle peer close/unregister/reopen and fallback connections
- */
+static void maweb_disconnect(instance* inst);
static uint64_t last_keepalive = 0;
-static uint64_t update_interval = 50;
+static uint64_t update_interval = 0;
static uint64_t last_update = 0;
-static uint64_t updates_inflight = 0;
static uint64_t quiet_mode = 0;
static maweb_command_key cmdline_keys[] = {
@@ -136,7 +134,10 @@ static int channel_comparator(const void* raw_a, const void* raw_b){
}
static uint32_t maweb_interval(){
- return update_interval - (last_update % update_interval);
+ if(update_interval){
+ return update_interval - (last_update % update_interval);
+ }
+ return 0;
}
static int maweb_configure(char* option, char* value){
@@ -248,7 +249,7 @@ static int maweb_instance(instance* 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
+ .in = -1 //this hack allows the initial data request to push events even for zero'ed channels
};
char* next_token = NULL;
channel* channel_ref = NULL;
@@ -352,8 +353,7 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload
if(mmbackend_send(data->fd, frame_header, header_bytes)
|| mmbackend_send(data->fd, payload, len)){
LOGPF("Failed to send on instance %s, assuming connection failure", inst->name);
- data->state = ws_closed;
- data->login = 0;
+ maweb_disconnect(inst);
return 1;
}
@@ -423,6 +423,7 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty
}
static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, size_t payload_length){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
size_t base_offset = json_obj_offset(payload, "itemGroups"), group_offset, subgroup_offset, item_offset;
uint64_t group = 0, subgroup, item, metatype;
@@ -466,8 +467,9 @@ static int maweb_process_playbacks(instance* inst, int64_t page, char* payload,
}
group++;
}
- updates_inflight--;
- DBGPF("Playback message processing done, %" PRIu64 " updates inflight", updates_inflight);
+
+ data->updates_inflight--;
+ DBGPF("Playback message processing done, %" PRIu64 " updates inflight on %s", data->updates_inflight, inst->name);
return 0;
}
@@ -479,9 +481,9 @@ static int maweb_request_playbacks(instance* inst){
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){
+ if(data->updates_inflight){
if(quiet_mode < 1){
- LOGPF("Skipping update request, %" PRIu64 " updates still inflight - consider raising the interval time", updates_inflight);
+ LOGPF("Skipping update request on %s, %" PRIu64 " updates still inflight - consider raising the interval time", inst->name, data->updates_inflight);
}
return 0;
}
@@ -572,15 +574,16 @@ static int maweb_request_playbacks(instance* inst){
data->session);
maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
DBGPF("Poll request: %s", xmit_buffer);
- updates_inflight++;
+ data->updates_inflight++;
}
- DBGPF("Poll request handling done, %" PRIu64 " updates requested", updates_inflight);
+ DBGPF("Poll request handling done, %" PRIu64 " updates requested on %s", data->updates_inflight, inst->name);
return rv;
}
static int maweb_handle_message(instance* inst, char* payload, size_t payload_length){
char xmit_buffer[MAWEB_XMIT_CHUNK];
+ int64_t session = 0;
char* field;
maweb_instance_data* data = (maweb_instance_data*) inst->impl;
@@ -591,31 +594,50 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le
if(json_obj_bool(payload, "result", 0)){
LOG("Login successful");
data->login = 1;
+
+ //initially request playbacks
+ if(!update_interval){
+ maweb_request_playbacks(inst);
+ }
}
else{
- LOG("Login failed");
data->login = 0;
+
+ if(data->hosts > 1){
+ LOGPF("Console login failed on %s, will try again with the next host", inst->name);
+ maweb_disconnect(inst);
+ }
+ else{
+ LOGPF("Console login failed on %s", inst->name);
+ }
+ return 0;
}
}
if(!strncmp(field, "playbacks", 9)){
if(maweb_process_playbacks(inst, json_obj_int(payload, "iPage", 0), payload, payload_length)){
LOG("Failed to handle/request input data");
}
+
+ //request playbacks again if configured
+ if(!update_interval && data->login && !data->updates_inflight){
+ maweb_request_playbacks(inst);
+ }
return 0;
}
}
DBGPF("Incoming message (%" PRIsize_t "): %s", payload_length, payload);
if(json_obj(payload, "session") == JSON_NUMBER){
- data->session = json_obj_int(payload, "session", data->session);
- if(data->session < 0){
- LOG("Login failed");
- data->login = 0;
- return 0;
+ session = json_obj_int(payload, "session", data->session);
+ if(session < 0){
+ LOG("Invalid web remote session identifier received, closing connection");
+ maweb_disconnect(inst);
+ return 0;
}
- if(quiet_mode < 2){
- LOGPF("Session id is now %" PRId64, data->session);
+ if(data->session != session){
+ LOGPF("Web remote session ID changed from %" PRId64 " to %" PRId64 "", data->session, session);
}
+ data->session = session;
}
if(json_obj_bool(payload, "forceLogin", 0)){
@@ -642,6 +664,30 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le
return 0;
}
+static void maweb_disconnect(instance* inst){
+ maweb_instance_data* data = (maweb_instance_data*) inst->impl;
+ char xmit_buffer[MAWEB_XMIT_CHUNK];
+
+ if(data->fd){
+ //close the session if one is active
+ if(data->session > 0){
+ snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"close\",\"session\":%" PRIu64 "}", data->session);
+ maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer));
+ }
+
+ mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL);
+ close(data->fd);
+ }
+
+ data->fd = -1;
+ data->state = ws_closed;
+ data->login = 0;
+ data->session = -1;
+ data->peer_type = peer_unidentified;
+ data->offset = 0;
+ data->updates_inflight = 0;
+}
+
static int maweb_connect(instance* inst){
int rv = 1;
maweb_instance_data* data = (maweb_instance_data*) inst->impl;
@@ -650,14 +696,8 @@ static int maweb_connect(instance* inst){
goto bail;
}
- //unregister old fd from core
- if(data->fd >= 0){
- mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL);
- close(data->fd);
- data->fd = -1;
- }
- data->state = ws_closed;
- data->login = 0;
+ //close old connection and reset state
+ maweb_disconnect(inst);
LOGPF("Connecting to host %" PRIsize_t " of %" PRIsize_t " on %s", data->next_host + 1, data->hosts, inst->name);
@@ -1047,7 +1087,7 @@ static int maweb_handle(size_t num, managed_fd* fds){
last_keepalive = mm_timestamp();
}
- if(last_update && mm_timestamp() - last_update >= update_interval){
+ if(update_interval && last_update && mm_timestamp() - last_update >= update_interval){
rv |= maweb_poll();
last_update = mm_timestamp();
}
@@ -1071,7 +1111,7 @@ static int maweb_start(size_t n, instance** inst){
//re-set channel identifiers
for(p = 0; p < data->channels; p++){
- data->channel[p].chan->ident = p;
+ mm_channel_update(data->channel[p].chan, p);
}
//try to connect to any available host
@@ -1114,14 +1154,10 @@ static int maweb_shutdown(size_t n, instance** inst){
free(data->pass);
data->pass = NULL;
- close(data->fd);
- data->fd = -1;
-
+ maweb_disconnect(inst[u]);
free(data->buffer);
data->buffer = NULL;
-
- data->offset = data->allocated = 0;
- data->state = ws_closed;
+ data->allocated = 0;
free(data->channel);
data->channel = NULL;
diff --git a/backends/maweb.h b/backends/maweb.h
index 85ca09d..8efe6a8 100644
--- a/backends/maweb.h
+++ b/backends/maweb.h
@@ -100,4 +100,6 @@ typedef struct /*_maweb_instance_data*/ {
size_t offset;
size_t allocated;
uint8_t* buffer;
+
+ uint64_t updates_inflight;
} maweb_instance_data;
diff --git a/backends/maweb.md b/backends/maweb.md
index 1547919..6ac2cd1 100644
--- a/backends/maweb.md
+++ b/backends/maweb.md
@@ -18,7 +18,7 @@ Web Remote. Set a web remote password using the option below the activation sett
| Option | Example value | Default value | Description |
|---------------|-----------------------|-----------------------|---------------------------------------------------------------|
-| `interval` | `100` | `50` | Query interval for input data polling (in msec). |
+| `interval` | `100` | `0` | Query interval for input data polling (in msec). If set to 0 (the default), data is queried again when the previous data request has received an answer. |
| `quiet` | `1` | `0` | Turn off some warning messages, for use by experts. |
#### Instance configuration
diff --git a/backends/midi.c b/backends/midi.c
index 1f0f2d5..4bf846a 100644
--- a/backends/midi.c
+++ b/backends/midi.c
@@ -13,7 +13,10 @@ enum /*_midi_channel_type*/ {
cc,
pressure,
aftertouch,
- pitchbend
+ pitchbend,
+ program,
+ rpn,
+ nrpn
};
static struct {
@@ -81,7 +84,7 @@ 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")){
+ if(!strcmp(option, "read") || !strcmp(option, "source")){
//connect input device
if(data->read){
LOGPF("Instance %s was already connected to an input device", inst->name);
@@ -90,7 +93,7 @@ static int midi_configure_instance(instance* inst, char* option, char* value){
data->read = strdup(value);
return 0;
}
- else if(!strcmp(option, "write")){
+ else if(!strcmp(option, "write") || !strcmp(option, "target")){
//connect output device
if(data->write){
LOGPF("Instance %s was already connected to an output device", inst->name);
@@ -99,8 +102,15 @@ static int midi_configure_instance(instance* inst, char* option, char* value){
data->write = strdup(value);
return 0;
}
+ else if(!strcmp(option, "epn-tx")){
+ data->epn_tx_short = 0;
+ if(!strcmp(value, "short")){
+ data->epn_tx_short = 1;
+ }
+ return 0;
+ }
- LOGPF("Unknown instance option %s", option);
+ LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name);
return 1;
}
@@ -147,9 +157,20 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){
ident.fields.type = pressure;
channel += 8;
}
+ else if(!strncmp(channel, "rpn", 3)){
+ ident.fields.type = rpn;
+ channel += 3;
+ }
+ else if(!strncmp(channel, "nrpn", 4)){
+ ident.fields.type = nrpn;
+ channel += 4;
+ }
else if(!strncmp(channel, "pitch", 5)){
ident.fields.type = pitchbend;
}
+ else if(!strncmp(channel, "program", 7)){
+ ident.fields.type = program;
+ }
else if(!strncmp(channel, "aftertouch", 10)){
ident.fields.type = aftertouch;
}
@@ -167,9 +188,40 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){
return NULL;
}
+static void midi_tx(int port, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){
+ snd_seq_event_t ev;
+
+ snd_seq_ev_clear(&ev);
+ snd_seq_ev_set_source(&ev, port);
+ snd_seq_ev_set_subs(&ev);
+ snd_seq_ev_set_direct(&ev);
+
+ switch(type){
+ case note:
+ snd_seq_ev_set_noteon(&ev, channel, control, value);
+ break;
+ case cc:
+ snd_seq_ev_set_controller(&ev, channel, control, value);
+ break;
+ case pressure:
+ snd_seq_ev_set_keypress(&ev, channel, control, value);
+ break;
+ case pitchbend:
+ snd_seq_ev_set_pitchbend(&ev, channel, value);
+ break;
+ case aftertouch:
+ snd_seq_ev_set_chanpress(&ev, channel, value);
+ break;
+ case program:
+ snd_seq_ev_set_pgmchange(&ev, channel, value);
+ break;
+ }
+
+ snd_seq_event_output(sequencer, &ev);
+}
+
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*) inst->impl;
midi_channel_ident ident = {
.label = 0
@@ -178,30 +230,29 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){
for(u = 0; u < num; u++){
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);
- break;
- 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);
+ case rpn:
+ case nrpn:
+ //transmit parameter number
+ midi_tx(data->port, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control >> 7) & 0x7F);
+ midi_tx(data->port, cc, ident.fields.channel, (ident.fields.type == rpn) ? 100 : 98, ident.fields.control & 0x7F);
+ //transmit parameter value
+ midi_tx(data->port, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) >> 7) & 0x7F);
+ midi_tx(data->port, cc, ident.fields.channel, 38, ((uint16_t) (v[u].normalised * 16383.0)) & 0x7F);
+
+ if(!data->epn_tx_short){
+ //clear active parameter
+ midi_tx(data->port, cc, ident.fields.channel, 101, 127);
+ midi_tx(data->port, cc, ident.fields.channel, 100, 127);
+ }
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);
+ //TODO check whether this actually works that well
+ midi_tx(data->port, ident.fields.type, ident.fields.channel, ident.fields.control, (v[u].normalised * 16383.0) - 8192);
break;
+ default:
+ midi_tx(data->port, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0);
}
-
- snd_seq_event_output(sequencer, &ev);
}
snd_seq_drain_output(sequencer);
@@ -216,21 +267,108 @@ static char* midi_type_name(uint8_t type){
return "note";
case cc:
return "cc";
+ case rpn:
+ return "rpn";
+ case nrpn:
+ return "nrpn";
case pressure:
return "pressure";
case aftertouch:
return "aftertouch";
case pitchbend:
return "pitch";
+ case program:
+ return "program";
}
return "unknown";
}
+//this state machine is used more-or-less verbatim in the winmidi, rtpmidi and jack backends - fixes need to be applied there, too
+static void midi_handle_epn(instance* inst, uint8_t chan, uint16_t control, uint16_t value){
+ midi_instance_data* data = (midi_instance_data*) inst->impl;
+ midi_channel_ident ident = {
+ .label = 0
+ };
+ channel* changed = NULL;
+ channel_value val;
+ //check for 3-byte update TODO
+
+ //switching between nrpn and rpn clears all valid bits
+ if(((data->epn_status[chan] & EPN_NRPN) && (control == 101 || control == 100))
+ || (!(data->epn_status[chan] & EPN_NRPN) && (control == 99 || control == 98))){
+ data->epn_status[chan] &= ~(EPN_NRPN | EPN_PARAMETER_LO | EPN_PARAMETER_HI);
+ }
+
+ //setting an address always invalidates the value valid bits
+ if(control >= 98 && control <= 101){
+ data->epn_status[chan] &= ~(EPN_VALUE_HI /*| EPN_VALUE_LO*/);
+ }
+
+ //parameter hi
+ if(control == 101 || control == 99){
+ data->epn_control[chan] &= 0x7F;
+ data->epn_control[chan] |= value << 7;
+ data->epn_status[chan] |= EPN_PARAMETER_HI | ((control == 99) ? EPN_NRPN : 0);
+ if(control == 101 && value == 127){
+ data->epn_status[chan] &= ~EPN_PARAMETER_HI;
+ }
+ }
+
+ //parameter lo
+ if(control == 100 || control == 98){
+ data->epn_control[chan] &= ~0x7F;
+ data->epn_control[chan] |= value & 0x7F;
+ data->epn_status[chan] |= EPN_PARAMETER_LO | ((control == 98) ? EPN_NRPN : 0);
+ if(control == 100 && value == 127){
+ data->epn_status[chan] &= ~EPN_PARAMETER_LO;
+ }
+ }
+
+ //value hi, clears low, mark as update candidate
+ if(control == 6
+ //check if parameter is set before accepting value update
+ && ((data->epn_status[chan] & (EPN_PARAMETER_HI | EPN_PARAMETER_LO)) == (EPN_PARAMETER_HI | EPN_PARAMETER_LO))){
+ data->epn_value[chan] = value << 7;
+ data->epn_status[chan] |= EPN_VALUE_HI;
+ }
+
+ //FIXME is the update order for the value bits fixed?
+ //FIXME can there be standalone updates on CC 38?
+
+ //value lo, flush the value
+ if(control == 38
+ && data->epn_status[chan] & EPN_VALUE_HI){
+ data->epn_value[chan] &= ~0x7F;
+ data->epn_value[chan] |= value & 0x7F;
+ //FIXME not clearing the valid bit would allow for fast low-order updates
+ data->epn_status[chan] &= ~EPN_VALUE_HI;
+
+ if(midi_config.detect){
+ LOGPF("Incoming EPN data on channel %s.ch%d.%s%d", inst->name, chan, data->epn_status[chan] & EPN_NRPN ? "nrpn" : "rpn", data->epn_control[chan]);
+ }
+
+ //find the updated channel
+ ident.fields.type = data->epn_status[chan] & EPN_NRPN ? nrpn : rpn;
+ ident.fields.channel = chan;
+ ident.fields.control = data->epn_control[chan];
+ val.normalised = (double) data->epn_value[chan] / 16383.0;
+
+ //push the new value
+ changed = mm_channel(inst, ident.label, 0);
+ if(changed){
+ mm_channel_event(changed, val);
+ }
+ }
+}
+
static int midi_handle(size_t num, managed_fd* fds){
snd_seq_event_t* ev = NULL;
instance* inst = NULL;
+ midi_instance_data* data = NULL;
+
channel* changed = NULL;
channel_value val;
+
char* event_type = NULL;
midi_channel_ident ident = {
.label = 0
@@ -248,6 +386,14 @@ static int midi_handle(size_t num, managed_fd* fds){
ident.fields.control = ev->data.note.note;
val.normalised = (double) ev->data.note.velocity / 127.0;
+ //scan for the instance before parsing incoming data, instance state is required for the EPN state machine
+ inst = mm_instance_find(BACKEND_NAME, ev->dest.port);
+ if(!inst){
+ LOG("Delivered event did not match any instance");
+ continue;
+ }
+ data = (midi_instance_data*) inst->impl;
+
switch(ev->type){
case SND_SEQ_EVENT_NOTEON:
case SND_SEQ_EVENT_NOTEOFF:
@@ -263,18 +409,35 @@ static int midi_handle(size_t num, managed_fd* fds){
case SND_SEQ_EVENT_CHANPRESS:
ident.fields.type = aftertouch;
ident.fields.channel = ev->data.control.channel;
+ ident.fields.control = 0;
val.normalised = (double) ev->data.control.value / 127.0;
break;
case SND_SEQ_EVENT_PITCHBEND:
ident.fields.type = pitchbend;
+ ident.fields.control = 0;
ident.fields.channel = ev->data.control.channel;
val.normalised = ((double) ev->data.control.value + 8192) / 16383.0;
break;
+ case SND_SEQ_EVENT_PGMCHANGE:
+ ident.fields.type = program;
+ ident.fields.control = 0;
+ ident.fields.channel = ev->data.control.channel;
+ val.normalised = (double) ev->data.control.value / 127.0;
+ break;
case SND_SEQ_EVENT_CONTROLLER:
ident.fields.type = cc;
ident.fields.channel = ev->data.control.channel;
ident.fields.control = ev->data.control.param;
val.normalised = (double) ev->data.control.value / 127.0;
+
+ //check for EPN CCs and update the state machine
+ if((ident.fields.control <= 101 && ident.fields.control >= 98)
+ || ident.fields.control == 6
+ || ident.fields.control == 38
+ //if the high-order value bits are set, forward any control to the state machine for the short update form
+ || data->epn_status[ident.fields.channel] & EPN_VALUE_HI){
+ midi_handle_epn(inst, ident.fields.channel, ident.fields.control, ev->data.control.value);
+ }
break;
default:
LOG("Ignored event of unsupported type");
@@ -282,13 +445,6 @@ static int midi_handle(size_t num, managed_fd* fds){
}
event_type = midi_type_name(ident.fields.type);
- inst = mm_instance_find(BACKEND_NAME, ev->dest.port);
- if(!inst){
- //FIXME might want to return failure
- LOG("Delivered event did not match any instance");
- continue;
- }
-
changed = mm_channel(inst, ident.label, 0);
if(changed){
if(mm_channel_event(changed, val)){
@@ -298,7 +454,7 @@ static int midi_handle(size_t num, managed_fd* fds){
}
if(midi_config.detect && event_type){
- if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){
+ if(ident.fields.type == pitchbend || ident.fields.type == aftertouch || ident.fields.type == program){
LOGPF("Incoming data on channel %s.ch%d.%s", inst->name, ident.fields.channel, event_type);
}
else{
diff --git a/backends/midi.h b/backends/midi.h
index dcee010..e2d6543 100644
--- a/backends/midi.h
+++ b/backends/midi.h
@@ -10,18 +10,28 @@ static int midi_handle(size_t num, managed_fd* fds);
static int midi_start(size_t n, instance** inst);
static int midi_shutdown(size_t n, instance** inst);
+#define EPN_NRPN 8
+#define EPN_PARAMETER_HI 4
+#define EPN_PARAMETER_LO 2
+#define EPN_VALUE_HI 1
+
typedef struct /*_midi_instance_data*/ {
int port;
char* read;
char* write;
+
+ uint8_t epn_tx_short;
+ uint16_t epn_control[16];
+ uint16_t epn_value[16];
+ uint8_t epn_status[16];
} midi_instance_data;
typedef union {
struct {
- uint8_t pad[5];
+ uint8_t pad[4];
uint8_t type;
uint8_t channel;
- uint8_t control;
+ uint16_t control;
} fields;
uint64_t label;
} midi_channel_ident;
diff --git a/backends/midi.md b/backends/midi.md
index d3d6e33..6280205 100644
--- a/backends/midi.md
+++ b/backends/midi.md
@@ -11,10 +11,11 @@ The MIDI backend provides read-write access to the MIDI protocol via virtual por
#### 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 |
+| Option | Example value | Default value | Description |
+|-----------------------|-----------------------|-----------------------|-----------------------|
+| `read` / `source` | `20:0` | none | MIDI device to connect for input |
+| `write` / `target` | `DeviceName` | none | MIDI device to connect for output |
+| `epn-tx` | `short` | `full` | Configures whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter |
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.
@@ -30,25 +31,39 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels.
* `pressure` - Note pressure/aftertouch messages
* `aftertouch` - Channel-wide aftertouch messages
* `pitch` - Channel pitchbend messages
+* `program` - Channel program change messages
+* `rpn` - Registered parameter numbers (14-bit extension)
+* `nrpn` - Non-registered parameter numbers (14-bit extension)
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>`.
+The `pitch`, `aftertouch` and `program` messages/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.
+Every MIDI channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using
+the CC controls 101/100/99/98/38/6. Both control types have 14-bit IDs and 14-bit values.
+
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
+midi1.ch0.nrpn900 > midi2.ch0.rpn1
+midi2.ch15.note1 > midi1.ch2.program
```
#### Known bugs / problems
+Extended parameter numbers (EPNs, the `rpn` and `nrpn` control types) will also generate events on the controls (CC 101 through
+98, 38 and 6) that are used as the lower layer transport. When using EPNs, mapping those controls is probably not useful.
+
+EPN control types support only the full 14-bit transfer encoding, not the shorter variant transmitting only the 7
+high-order bits. This may be changed if there is sufficient interest in the functionality.
+
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.
diff --git a/backends/mqtt.c b/backends/mqtt.c
new file mode 100644
index 0000000..da4bf38
--- /dev/null
+++ b/backends/mqtt.c
@@ -0,0 +1,1006 @@
+#define BACKEND_NAME "mqtt"
+//#define DEBUG
+
+#include <string.h>
+#include <time.h>
+#include <math.h>
+
+#include "libmmbackend.h"
+#include "mqtt.h"
+
+static uint64_t last_maintenance = 0;
+/* according to spec 2.2.2.2 */
+static struct {
+ uint8_t property;
+ uint8_t storage;
+} property_lengths[] = {
+ {0x01, STORAGE_U8},
+ {0x02, STORAGE_U32},
+ {0x03, STORAGE_PREFIXED},
+ {0x08, STORAGE_PREFIXED},
+ {0x09, STORAGE_PREFIXED},
+ {0x0B, STORAGE_VARINT},
+ {0x11, STORAGE_U32},
+
+ {0x12, STORAGE_PREFIXED},
+ {0x13, STORAGE_U16},
+ {0x15, STORAGE_PREFIXED},
+ {0x16, STORAGE_PREFIXED},
+ {0x17, STORAGE_U8},
+ {0x18, STORAGE_U32},
+ {0x19, STORAGE_U8},
+ {0x1A, STORAGE_PREFIXED},
+ {0x1C, STORAGE_PREFIXED},
+ {0x1F, STORAGE_PREFIXED},
+ {0x21, STORAGE_U16},
+ {0x22, STORAGE_U16},
+ {0x23, STORAGE_U16},
+ {0x24, STORAGE_U8},
+ {0x25, STORAGE_U8},
+ {0x26, STORAGE_PREFIXPAIR},
+ {0x27, STORAGE_U32},
+ {0x28, STORAGE_U8},
+ {0x29, STORAGE_U8},
+ {0x2A, STORAGE_U8}
+};
+
+/*
+ * TODO
+ * * proper RETAIN handling
+ * * TLS
+ * * JSON subchannels
+ */
+
+MM_PLUGIN_API int init(){
+ backend mqtt = {
+ .name = BACKEND_NAME,
+ .conf = mqtt_configure,
+ .create = mqtt_instance,
+ .conf_instance = mqtt_configure_instance,
+ .channel = mqtt_channel,
+ .handle = mqtt_set,
+ .process = mqtt_handle,
+ .start = mqtt_start,
+ .shutdown = mqtt_shutdown
+ };
+
+ //register backend
+ if(mm_backend_register(mqtt)){
+ LOG("Failed to register backend");
+ return 1;
+ }
+ return 0;
+}
+
+static int mqtt_parse_hostspec(instance* inst, char* hostspec){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ char* host = strchr(hostspec, '@'), *password = NULL, *port = NULL;
+
+ //mqtt[s]://[username][:password]@host.domain[:port]
+ if(!strncmp(hostspec, "mqtt://", 7)){
+ hostspec += 7;
+ }
+ else if(!strncmp(hostspec, "mqtts://", 8)){
+ data->tls = 1;
+ hostspec += 8;
+ }
+
+ if(host){
+ //parse credentials, separate out host spec
+ *host = 0;
+ host++;
+
+ password = strchr(hostspec, ':');
+ if(password){
+ //password supplied, store
+ *password = 0;
+ password++;
+ mmbackend_strdup(&(data->password), password);
+ }
+
+ //store username
+ mmbackend_strdup(&(data->user), hostspec);
+ }
+ else{
+ host = hostspec;
+ }
+
+ //parse port if supplied
+ port = strchr(host, ':');
+ if(port){
+ *port = 0;
+ port++;
+ mmbackend_strdup(&(data->port), port);
+ }
+
+ mmbackend_strdup(&(data->host), host);
+ return 0;
+}
+
+static int mqtt_generate_instanceid(instance* inst){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ char clientid[24] = "";
+
+ snprintf(clientid, sizeof(clientid), "MIDIMonster-%d-%s", (uint32_t) time(NULL), inst->name);
+ return mmbackend_strdup(&(data->client_id), clientid);
+}
+
+static size_t mqtt_pop_varint(uint8_t* buffer, size_t len, uint32_t* result){
+ size_t value = 0, offset = 0;
+ do {
+ if(offset >= len){
+ return 0;
+ }
+
+ value |= (buffer[offset] & 0x7F) << (7 * offset);
+ offset++;
+ } while(buffer[offset - 1] & 0x80);
+
+ if(result){
+ *result = value;
+ }
+ return offset;
+}
+
+static size_t mqtt_pop_property(uint8_t* buffer, size_t bytes){
+ size_t length = 0, u;
+
+ if(bytes){
+ for(u = 0; u < sizeof(property_lengths)/sizeof(property_lengths[0]); u++){
+ if(property_lengths[u].property == buffer[0]){
+ switch(property_lengths[u].storage){
+ case STORAGE_U8:
+ return 2;
+ case STORAGE_U16:
+ return 3;
+ case STORAGE_U32:
+ return 5;
+ case STORAGE_VARINT:
+ return mqtt_pop_varint(buffer + 1, bytes - 1, NULL) + 1;
+ case STORAGE_PREFIXED:
+ if(bytes >= 3){
+ return ((buffer[1] << 8) | buffer[2]) + 1;
+ }
+ //best-effort guess
+ return 3;
+ case STORAGE_PREFIXPAIR:
+ if(bytes >= 3){
+ length = ((buffer[1] << 8) | buffer[2]);
+ if(bytes >= length + 5){
+ return (1 + 2 + length + 2 + ((buffer[length + 3] << 8) | buffer[length + 4]));
+ }
+ return length + 3;
+ }
+ //best-effort guess
+ return 5;
+ }
+ }
+ }
+ }
+
+ LOGPF("Storage class for property %02X was unknown", buffer[0]);
+ return 1;
+}
+
+static size_t mqtt_push_varint(size_t value, size_t maxlen, uint8_t* buffer){
+ //implementation conforming to spec 1.5.5
+ size_t offset = 0;
+ do {
+ buffer[offset] = value % 128;
+ value = value / 128;
+ if(value){
+ buffer[offset] |= 0x80;
+ }
+ offset++;
+ } while(value);
+ return offset;
+}
+
+static size_t mqtt_push_binary(uint8_t* buffer, size_t buffer_length, uint8_t* content, size_t length){
+ if(buffer_length < length + 2 || length > 65535){
+ LOG("Failed to push length-prefixed data blob, buffer size exceeded");
+ return 0;
+ }
+
+ buffer[0] = (length >> 8) & 0xFF;
+ buffer[1] = length & 0xFF;
+
+ memcpy(buffer + 2, content, length);
+ return length + 2;
+}
+
+static size_t mqtt_push_utf8(uint8_t* buffer, size_t buffer_length, char* content){
+ //FIXME might want to validate the string for valid UTF-8
+ return mqtt_push_binary(buffer, buffer_length, (uint8_t*) content, strlen(content));
+}
+
+static size_t mqtt_pop_utf8(uint8_t* buffer, size_t buffer_length, char** data){
+ size_t length = 0;
+ *data = NULL;
+
+ if(buffer_length < 2){
+ return 0;
+ }
+
+ length = (buffer[0] << 8) | buffer[1];
+ if(buffer_length >= length + 2){
+ *data = (char*) buffer + 2;
+ }
+ return length;
+}
+
+static void mqtt_disconnect(instance* inst){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ size_t u;
+
+ data->last_control = 0;
+
+ //reset aliases as they can not be reused across sessions
+ data->server_max_alias = 0;
+ data->current_alias = 1;
+ for(u = 0; u < data->nchannels; u++){
+ data->channel[u].topic_alias_sent = 0;
+ data->channel[u].topic_alias_rcvd = 0;
+ }
+
+ //unmanage the fd
+ mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL);
+
+ close(data->fd);
+ data->fd = -1;
+}
+
+static int mqtt_transmit(instance* inst, uint8_t type, size_t vh_length, uint8_t* vh, size_t payload_length, uint8_t* payload){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ uint8_t fixed_header[5];
+ size_t offset = 0;
+
+ //how in the world is it a _fixed_ header if it contains a variable length integer? eh...
+ fixed_header[offset++] = type;
+ offset += mqtt_push_varint(vh_length + payload_length, sizeof(fixed_header) - offset, fixed_header + offset);
+
+ if(mmbackend_send(data->fd, fixed_header, offset)
+ || (vh && vh_length && mmbackend_send(data->fd, vh, vh_length))
+ || (payload && payload_length && mmbackend_send(data->fd, payload, payload_length))){
+ LOGPF("Failed to transmit control message for %s, assuming connection failure", inst->name);
+ mqtt_disconnect(inst);
+ return 1;
+ }
+
+ data->last_control = mm_timestamp();
+ return 0;
+}
+
+static int mqtt_configure(char* option, char* value){
+ LOG("This backend does not take global configuration");
+ return 1;
+}
+
+static int mqtt_reconnect(instance* inst){
+ uint8_t variable_header[MQTT_BUFFER_LENGTH] = {0x00, 0x04, 'M', 'Q', 'T', 'T', MQTT_VERSION_DEFAULT, 0x00 /*flags*/, ((MQTT_KEEPALIVE * 2) >> 8) & 0xFF, (MQTT_KEEPALIVE * 2) & 0xFF};
+ uint8_t payload[MQTT_BUFFER_LENGTH];
+ size_t vh_offset = 10, payload_offset = 0;
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+
+ if(!data->host){
+ LOGPF("No host specified for instance %s", inst->name);
+ return 2;
+ }
+
+ if(data->fd >= 0){
+ mqtt_disconnect(inst);
+ }
+
+ LOGPF("Connecting instance %s to host %s port %s (TLS: %s, Authentication: %s, Protocol: %s)",
+ inst->name, data->host,
+ data->port ? data->port : (data->tls ? MQTT_TLS_PORT : MQTT_PORT),
+ data->tls ? "yes " : "no",
+ (data->user || data->password) ? "yes" : "no",
+ (data->mqtt_version == 0x05) ? "v5" : "v3.1.1");
+
+ data->fd = mmbackend_socket(data->host,
+ data->port ? data->port : (data->tls ? MQTT_TLS_PORT : MQTT_PORT),
+ SOCK_STREAM, 0, 0, 1);
+
+ if(data->fd < 0){
+ //retry later
+ return 1;
+ }
+
+ //prepare CONNECT message header
+ variable_header[6] = data->mqtt_version;
+ variable_header[7] = 0x02 /*clean start*/ | (data->user ? 0x80 : 0x00) | (data->user ? 0x40 : 0x00);
+
+ if(data->mqtt_version == 0x05){ //mqtt v5 has additional options
+ //push number of option bytes (as a varint, no less) before actually pushing the option data.
+ //obviously someone thought saving 3 whole bytes in exchange for not being able to sequentially creating the package was smart..
+ variable_header[vh_offset++] = 8;
+ //push maximum packet size option
+ variable_header[vh_offset++] = 0x27;
+ variable_header[vh_offset++] = (MQTT_BUFFER_LENGTH >> 24) & 0xFF;
+ variable_header[vh_offset++] = (MQTT_BUFFER_LENGTH >> 16) & 0xFF;
+ variable_header[vh_offset++] = (MQTT_BUFFER_LENGTH >> 8) & 0xFF;
+ variable_header[vh_offset++] = (MQTT_BUFFER_LENGTH) & 0xFF;
+ //push topic alias maximum option
+ variable_header[vh_offset++] = 0x22;
+ variable_header[vh_offset++] = 0xFF;
+ variable_header[vh_offset++] = 0xFF;
+ }
+
+ //prepare CONNECT payload
+ //push client id
+ payload_offset += mqtt_push_utf8(payload + payload_offset, sizeof(payload) - payload_offset, data->client_id);
+ if(data->user){
+ payload_offset += mqtt_push_utf8(payload + payload_offset, sizeof(payload) - payload_offset, data->user);
+ }
+ if(data->password){
+ payload_offset += mqtt_push_utf8(payload + payload_offset, sizeof(payload) - payload_offset, data->password);
+ }
+
+ mqtt_transmit(inst, MSG_CONNECT, vh_offset, variable_header, payload_offset, payload);
+
+ //register the fd
+ if(mm_manage_fd(data->fd, BACKEND_NAME, 1, (void*) inst)){
+ LOG("Failed to register FD");
+ return 2;
+ }
+
+ return 0;
+}
+
+static int mqtt_configure_channel(instance* inst, char* option, char* value){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ char* next_token = NULL;
+ channel* configure = NULL;
+ uint8_t mark = 0;
+ mqtt_channel_value config = {
+ 0
+ };
+
+ if(!strncmp(value, "range ", 6)){
+ //we support min > max for range configurations
+ value += 6;
+
+ config.min = strtod(value, &next_token);
+ if(value == next_token){
+ LOGPF("Failed to parse range preconfiguration for topic %s.%s", inst->name, option);
+ return 1;
+ }
+
+ config.max = strtod(next_token, &value);
+ if(value == next_token){
+ LOGPF("Failed to parse range preconfiguration for topic %s.%s", inst->name, option);
+ return 1;
+ }
+ }
+ else if(!strncmp(value, "discrete ", 9)){
+ value += 9;
+
+ for(; *value && isspace(*value); value++){
+ }
+ if(value[0] == '!'){
+ mark = 1;
+ value++;
+ }
+ config.min = clamp(strtod(value, &next_token), 1.0, 0.0);
+ value = next_token;
+
+ for(; *value && isspace(*value); value++){
+ }
+ if(value[0] == '!'){
+ mark = 2;
+ value++;
+ }
+
+ config.max = clamp(strtod(value, &next_token), 1.0, 0.0);
+ value = next_token;
+ if(config.max < config.min){
+ LOGPF("Discrete topic configuration for %s.%s has invalid limit ordering", inst->name, option);
+ return 1;
+ }
+
+ for(; *value && isspace(*value); value++){
+ }
+
+ config.discrete = strdup(value);
+ config.normal = mark ? ((mark == 1) ? config.min : config.max) : (config.min + (config.max - config.min) / 2);
+ }
+ else{
+ LOGPF("Unknown instance configuration option or invalid preconfiguration %s on instance %s", option, inst->name);
+ return 1;
+ }
+
+ configure = mqtt_channel(inst, option, 0);
+ if(!configure
+ //if configuring scale, no other config is possible
+ || (!config.discrete && data->channel[configure->ident].values)
+ //if configuring discrete, the previous one can't be a a scale
+ || (config.discrete && data->channel[configure->ident].values && !data->channel[configure->ident].value[0].discrete)){
+ LOGPF("Failed to configure topic %s.%s", inst->name, option);
+ free(config.discrete);
+ return 1;
+ }
+
+ data->channel[configure->ident].value = realloc(data->channel[configure->ident].value, (data->channel[configure->ident].values + 1) * sizeof(mqtt_channel_value));
+ if(!data->channel[configure->ident].value){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ DBGPF("Configuring value on %s.%s: min %f max %f normal %f discrete %s", inst->name, option, config.min, config.max, config.normal, config.discrete ? config.discrete : "-");
+ data->channel[configure->ident].value[data->channel[configure->ident].values] = config;
+ data->channel[configure->ident].values++;
+ DBGPF("Value configuration for %s.%s now at %" PRIsize_t " entries", inst->name, option, data->channel[configure->ident].values);
+ return 0;
+}
+
+static int mqtt_configure_instance(instance* inst, char* option, char* value){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+
+ if(!strcmp(option, "user")){
+ mmbackend_strdup(&(data->user), value);
+ return 0;
+ }
+ else if(!strcmp(option, "password")){
+ mmbackend_strdup(&(data->password), value);
+ return 0;
+ }
+ else if(!strcmp(option, "host")){
+ if(mqtt_parse_hostspec(inst, value)){
+ return 1;
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "clientid")){
+ if(strlen(value)){
+ mmbackend_strdup(&(data->client_id), value);
+ return 0;
+ }
+ else{
+ return mqtt_generate_instanceid(inst);
+ }
+ }
+ else if(!strcmp(option, "protocol")){
+ data->mqtt_version = MQTT_VERSION_DEFAULT;
+ if(!strcmp(value, "3.1.1")){
+ data->mqtt_version = 4;
+ }
+ return 0;
+ }
+
+ //try to register as channel preconfig
+ return mqtt_configure_channel(inst, option, value);
+}
+
+static int mqtt_push_subscriptions(instance* inst){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ uint8_t variable_header[3] = {0};
+ uint8_t payload[MQTT_BUFFER_LENGTH];
+ size_t u, subs = 0, payload_offset = 0;
+
+ //FIXME might want to aggregate multiple subscribes into one packet
+ for(u = 0; u < data->nchannels; u++){
+ payload_offset = 0;
+ if(data->channel[u].flags & mmchannel_input){
+ DBGPF("Subscribing %s.%s, channel %" PRIsize_t ", flags %d", inst->name, data->channel[u].topic, u, data->channel[u].flags);
+ variable_header[0] = (data->packet_identifier >> 8) & 0xFF;
+ variable_header[1] = (data->packet_identifier) & 0xFF;
+
+ payload_offset += mqtt_push_utf8(payload + payload_offset, sizeof(payload) - payload_offset, data->channel[u].topic);
+ payload[payload_offset++] = (data->mqtt_version == 0x05) ? MQTT5_NO_LOCAL : 0;
+
+ data->packet_identifier++;
+ //zero is not a valid packet identifier
+ if(!data->packet_identifier){
+ data->packet_identifier++;
+ }
+
+ mqtt_transmit(inst, MSG_SUBSCRIBE, data->mqtt_version == 0x05 ? 3 : 2, variable_header, payload_offset, payload);
+ subs++;
+ }
+ }
+
+ LOGPF("Subscribed %" PRIsize_t " channels on %s", subs, inst->name);
+ return 0;
+}
+
+static int mqtt_instance(instance* inst){
+ mqtt_instance_data* data = calloc(1, sizeof(mqtt_instance_data));
+
+ if(!data){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ data->fd = -1;
+ data->mqtt_version = MQTT_VERSION_DEFAULT;
+ data->packet_identifier = 1;
+ data->current_alias = 1;
+ inst->impl = data;
+
+ if(mqtt_generate_instanceid(inst)){
+ return 1;
+ }
+ return 0;
+}
+
+static channel* mqtt_channel(instance* inst, char* spec, uint8_t flags){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ size_t u;
+
+ //check spec for compliance
+ if(strchr(spec, '+') || strchr(spec, '#')){
+ LOGPF("Invalid character in channel specification %s", spec);
+ return NULL;
+ }
+
+ //find matching channel
+ for(u = 0; u < data->nchannels; u++){
+ if(!strcmp(spec, data->channel[u].topic)){
+ data->channel[u].flags |= flags;
+ DBGPF("Reusing existing channel %" PRIsize_t " for spec %s.%s, flags are now %02X", u, inst->name, spec, data->channel[u].flags);
+ break;
+ }
+ }
+
+ //allocate new channel
+ if(u == data->nchannels){
+ data->channel = realloc(data->channel, (data->nchannels + 1) * sizeof(mqtt_channel_data));
+ if(!data->channel){
+ LOG("Failed to allocate memory");
+ return NULL;
+ }
+
+ data->channel[u].topic = strdup(spec);
+ data->channel[u].topic_alias_sent = 0;
+ data->channel[u].topic_alias_rcvd = 0;
+ data->channel[u].flags = flags;
+ data->channel[u].values = 0;
+ data->channel[u].value = NULL;
+
+ if(!data->channel[u].topic){
+ LOG("Failed to allocate memory");
+ return NULL;
+ }
+
+ DBGPF("Allocated channel %" PRIsize_t " for spec %s.%s, flags are %02X", u, inst->name, spec, data->channel[u].flags);
+ data->nchannels++;
+ }
+
+ return mm_channel(inst, u, 1);
+}
+
+static int mqtt_maintenance(){
+ size_t n, u;
+ instance** inst = NULL;
+ mqtt_instance_data* data = NULL;
+
+ if(mm_backend_instances(BACKEND_NAME, &n, &inst)){
+ LOG("Failed to fetch instance list");
+ return 1;
+ }
+
+ DBGPF("Running maintenance operations on %" PRIsize_t " instances", n);
+ for(u = 0; u < n; u++){
+ data = (mqtt_instance_data*) inst[u]->impl;
+ if(data->fd <= 0){
+ if(mqtt_reconnect(inst[u]) >= 2){
+ LOGPF("Failed to reconnect instance %s, terminating", inst[u]->name);
+ free(inst);
+ return 1;
+ }
+ }
+ else if(data->last_control && mm_timestamp() - data->last_control >= MQTT_KEEPALIVE * 1000){
+ //send keepalive ping requests
+ mqtt_transmit(inst[u], MSG_PINGREQ, 0, NULL, 0, NULL);
+ }
+ }
+
+ free(inst);
+ return 0;
+}
+
+static int mqtt_deserialize(instance* inst, channel* output, mqtt_channel_data* input, char* buffer, size_t length){
+ char* next_token = NULL, conversion_buffer[1024] = {0};
+ channel_value val;
+ double range, raw;
+ size_t u;
+ //FIXME implement json subchannels
+
+ //unconfigured channel
+ if(!input->values){
+ //the original buffer is the result of an unterminated receive, move it over
+ memcpy(conversion_buffer, buffer, length);
+ val.normalised = clamp(strtod(conversion_buffer, &next_token), 1.0, 0.0);
+ if(conversion_buffer == next_token){
+ LOGPF("Failed to parse incoming data for %s.%s", inst->name, input->topic);
+ return 1;
+ }
+ }
+ //ranged channel
+ else if(!input->value[0].discrete){
+ memcpy(conversion_buffer, buffer, length);
+ raw = clamp(strtod(conversion_buffer, &next_token), max(input->value[0].max, input->value[0].min), min(input->value[0].max, input->value[0].min));
+ if(conversion_buffer == next_token){
+ LOGPF("Failed to parse incoming data for %s.%s", inst->name, input->topic);
+ return 1;
+ }
+ range = fabs(input->value[0].max - input->value[0].min);
+ val.normalised = (raw - input->value[0].min) / range;
+ if(input->value[0].max < input->value[0].min){
+ val.normalised = fabs(val.normalised);
+ }
+ }
+ else{
+ for(u = 0; u < input->values; u++){
+ if(length == strlen(input->value[u].discrete)
+ && !strncmp(input->value[u].discrete, buffer, length)){
+ val.normalised = input->value[u].normal;
+ break;
+ }
+ }
+
+ if(u == input->values){
+ LOGPF("Failed to parse incoming data for %s.%s, no matching discrete token", inst->name, input->topic);
+ return 1;
+ }
+ }
+
+ val.normalised = clamp(val.normalised, 1.0, 0.0);
+ mm_channel_event(output, val);
+ return 0;
+}
+
+static size_t mqtt_serialize(instance* inst, mqtt_channel_data* input, char* output, size_t length, double value){
+ double range;
+ size_t u, invert = 0;
+
+ //unconfigured channel
+ if(!input->values){
+ return snprintf(output, length, "%f", value);
+ }
+ //ranged channel
+ else if(!input->value[0].discrete){
+ range = fabs(input->value[0].max - input->value[0].min);
+ if(input->value[0].max < input->value[0].min){
+ invert = 1;
+ }
+ return snprintf(output, length, "%f", (value * range) * (invert ? -1 : 1) + input->value[0].min);
+ }
+ else{
+ for(u = 0; u < input->values; u++){
+ if(input->value[u].min <= value
+ && input->value[u].max >= value){
+ memcpy(output, input->value[u].discrete, min(strlen(input->value[u].discrete), length));
+ return min(strlen(input->value[u].discrete), length);
+ }
+ }
+ }
+
+ LOGPF("No discrete value on %s.%s defined for normalized value %f", inst->name, input->topic, value);
+ return 0;
+}
+
+static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ uint8_t variable_header[MQTT_BUFFER_LENGTH];
+ uint8_t payload[MQTT_BUFFER_LENGTH], alias_assigned = 0;
+ size_t vh_length = 0, payload_length = 0, u;
+
+ for(u = 0; u < num; u++){
+ vh_length = payload_length = alias_assigned = 0;
+
+ if(data->mqtt_version == 0x05){
+ if(data->channel[c[u]->ident].topic_alias_sent){
+ //push zero-length topic
+ variable_header[vh_length++] = 0;
+ variable_header[vh_length++] = 0;
+ }
+ else{
+ //push topic
+ vh_length += mqtt_push_utf8(variable_header + vh_length, sizeof(variable_header) - vh_length, data->channel[c[u]->ident].topic);
+ //generate topic alias if possible
+ if(data->current_alias <= data->server_max_alias){
+ data->channel[c[u]->ident].topic_alias_sent = data->current_alias++;
+ DBGPF("Assigned outbound topic alias %" PRIu16 " to topic %s.%s", data->channel[c[u]->ident].topic_alias_sent, inst->name, data->channel[c[u]->ident].topic);
+
+ alias_assigned = 1;
+ }
+ }
+
+ //push property length
+ variable_header[vh_length++] = (data->channel[c[u]->ident].topic_alias_sent) ? 5 : 2;
+
+ //push payload type (0x01)
+ variable_header[vh_length++] = 0x01;
+ variable_header[vh_length++] = 1;
+
+ if(data->channel[c[u]->ident].topic_alias_sent){
+ //push topic alias (0x23)
+ variable_header[vh_length++] = 0x23;
+ variable_header[vh_length++] = (data->channel[c[u]->ident].topic_alias_sent >> 8) & 0xFF;
+ variable_header[vh_length++] = data->channel[c[u]->ident].topic_alias_sent & 0xFF;
+ }
+
+ payload_length = mqtt_serialize(inst, data->channel + c[u]->ident, (char*) (payload + 2), sizeof(payload) - 2, v[u].normalised);
+ if(payload_length){
+ payload[0] = (payload_length >> 8) & 0xFF;
+ payload[1] = payload_length & 0xFF;
+ payload_length += 2;
+ }
+ }
+ else{
+ //push topic
+ vh_length += mqtt_push_utf8(variable_header + vh_length, sizeof(variable_header) - vh_length, data->channel[c[u]->ident].topic);
+ if(data->mqtt_version == 0x05){
+ //push property length
+ variable_header[vh_length++] = 2;
+
+ //push payload type (0x01)
+ variable_header[vh_length++] = 0x01;
+ variable_header[vh_length++] = 1;
+ }
+ payload_length = mqtt_serialize(inst, data->channel + c[u]->ident, (char*) payload, sizeof(payload), v[u].normalised);
+ }
+
+ if(payload_length){
+ DBGPF("Transmitting %" PRIsize_t " bytes for %s", payload_length, inst->name);
+ mqtt_transmit(inst, MSG_PUBLISH, vh_length, variable_header, payload_length, payload);
+ }
+ else if(alias_assigned){
+ //undo alias assignment
+ data->channel[c[u]->ident].topic_alias_sent = 0;
+ data->current_alias--;
+ }
+ }
+
+ return 0;
+}
+
+static int mqtt_handle_publish(instance* inst, uint8_t type, uint8_t* variable_header, size_t length){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ char* topic = NULL, *payload = NULL;
+ channel* changed = NULL;
+ uint8_t qos = (type & 0x06) >> 1, content_utf8 = 0;
+ uint16_t topic_alias = 0;
+ uint32_t property_length = 0;
+ size_t u = data->nchannels, property_offset, payload_offset, payload_length;
+ size_t topic_length = min(mqtt_pop_utf8(variable_header, length, &topic), length);
+
+ property_offset = payload_offset = topic_length + 2 + ((qos > 0) ? 2 : 0);
+ if(data->mqtt_version == 0x05){
+ //read properties length
+ payload_offset += mqtt_pop_varint(variable_header + property_offset, length - property_offset, &property_length);
+ payload_offset += property_length;
+
+ property_offset += mqtt_pop_varint(variable_header + property_offset, length - property_offset, NULL);
+ //parse properties
+ while(property_offset < payload_offset && property_offset < length){
+ DBGPF("Property %02X at offset %" PRIsize_t " of %" PRIu32, variable_header[property_offset], property_offset, property_length);
+
+ //read payload format indicator
+ if(variable_header[property_offset] == 0x01 && property_offset < length - 1){
+ content_utf8 = variable_header[property_offset + 1];
+ }
+ //read topic alias
+ else if(variable_header[property_offset] == 0x23 && property_offset < length - 2){
+ topic_alias = (variable_header[property_offset + 1] << 8) | variable_header[property_offset + 2];
+ }
+
+ property_offset += mqtt_pop_property(variable_header + property_offset, length - property_offset);
+ }
+ }
+
+ //match via topic alias
+ if(!topic_length && topic_alias){
+ for(u = 0; u < data->nchannels; u++){
+ if(data->channel[u].topic_alias_rcvd == topic_alias){
+ break;
+ }
+ }
+ }
+ //match via topic
+ else if(topic_length){
+ for(u = 0; u < data->nchannels; u++){
+ if(!strncmp(data->channel[u].topic, topic, topic_length)){
+ break;
+ }
+ }
+
+ if(topic_alias){
+ data->channel[u].topic_alias_rcvd = topic_alias;
+ }
+ }
+
+ if(content_utf8){
+ payload_length = mqtt_pop_utf8(variable_header + payload_offset, length - payload_offset, &payload);
+ }
+ else{
+ payload_length = length - payload_offset;
+ payload = (char*) (variable_header + payload_offset);
+ }
+
+ if(u != data->nchannels && payload_length && payload){
+ DBGPF("Received PUBLISH for %s.%s, QoS %d, payload length %" PRIsize_t, inst->name, data->channel[u].topic, qos, payload_length);
+ changed = mm_channel(inst, u, 0);
+ if(changed){
+ mqtt_deserialize(inst, changed, data->channel + u, payload, payload_length);
+ }
+ }
+ return 0;
+}
+
+static int mqtt_handle_connack(instance* inst, uint8_t type, uint8_t* variable_header, size_t length){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ size_t property_offset = 2;
+
+ if(length >= 2){
+ if(variable_header[1]){
+ if(variable_header[1] == 1 && data->mqtt_version == 0x05){
+ LOGPF("Connection on %s was rejected for protocol incompatibility, downgrading to protocol 3.1.1", inst->name);
+ data->mqtt_version = 0x04;
+ return 0;
+ }
+ LOGPF("Connection on %s was rejected, reason code %d", inst->name, variable_header[1]);
+ mqtt_disconnect(inst);
+ return 0;
+ }
+
+ //parse response properties if present
+ if(data->mqtt_version == 0x05){
+ property_offset += mqtt_pop_varint(variable_header + property_offset, length - property_offset, NULL);
+ while(property_offset < length){
+ DBGPF("Property %02X at offset %" PRIsize_t " of %" PRIsize_t, variable_header[property_offset], property_offset, length);
+
+ //read maximum topic alias
+ if(variable_header[property_offset] == 0x22){
+ data->server_max_alias = (variable_header[property_offset + 1] << 8) | variable_header[property_offset + 2];
+ DBGPF("Connection supports maximum connection alias %" PRIu16, data->server_max_alias);
+ }
+
+ property_offset += mqtt_pop_property(variable_header + property_offset, length - property_offset);
+ }
+ }
+
+ LOGPF("Connection on %s established", inst->name);
+ return mqtt_push_subscriptions(inst);
+ }
+
+ LOGPF("Received malformed CONNACK on %s", inst->name);
+ return 1;
+}
+
+static int mqtt_handle_message(instance* inst, uint8_t type, uint8_t* variable_header, size_t length){
+ switch(type){
+ case MSG_CONNACK:
+ return mqtt_handle_connack(inst, type, variable_header, length);
+ case MSG_PINGRESP:
+ case MSG_SUBACK:
+ //ignore most responses
+ //FIXME error check SUBACK
+ break;
+ default:
+ if((type & 0xF0) == MSG_PUBLISH){
+ return mqtt_handle_publish(inst, type, variable_header, length);
+ }
+ LOGPF("Unhandled MQTT message type 0x%02X on %s", type, inst->name);
+ }
+ return 0;
+}
+
+static int mqtt_handle_fd(instance* inst){
+ mqtt_instance_data* data = (mqtt_instance_data*) inst->impl;
+ ssize_t bytes_read = 0, bytes_left = sizeof(data->receive_buffer) - data->receive_offset;
+ uint32_t message_length = 0, header_length = 0;
+
+ bytes_read = recv(data->fd, data->receive_buffer + data->receive_offset, bytes_left, 0);
+ if(bytes_read < 0){
+ LOGPF("Failed to receive data on instance %s: %s", inst->name, mmbackend_socket_strerror(errno));
+ return 1;
+ }
+ else if(bytes_read == 0){
+ //disconnected, try to reconnect
+ LOGPF("Instance %s disconnected, reconnection queued", inst->name);
+ mqtt_disconnect(inst);
+ return 1;
+ }
+
+ DBGPF("Instance %s, offset %" PRIsize_t ", read %" PRIsize_t " bytes", inst->name, data->receive_offset, bytes_read);
+ data->receive_offset += bytes_read;
+
+ while(data->receive_offset >= 2){
+ //check for complete message
+ header_length = mqtt_pop_varint(data->receive_buffer + 1, data->receive_offset - 1, &message_length);
+ if(header_length && data->receive_offset >= message_length + header_length + 1){
+ DBGPF("Received complete message of %" PRIu32 " bytes, total received %" PRIsize_t ", payload %" PRIu32 ", message type %02X", message_length + header_length + 1, data->receive_offset, message_length, data->receive_buffer[0]);
+ if(mqtt_handle_message(inst, data->receive_buffer[0], data->receive_buffer + header_length + 1, message_length)){
+ //TODO handle failures properly
+ }
+
+ //remove handled message
+ if(data->receive_offset > message_length + header_length + 1){
+ memmove(data->receive_buffer, data->receive_buffer + message_length + header_length + 1, data->receive_offset - (message_length + header_length + 1));
+ }
+ data->receive_offset -= message_length + header_length + 1;
+ }
+ else{
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static int mqtt_handle(size_t num, managed_fd* fds){
+ size_t n = 0;
+
+ for(n = 0; n < num; n++){
+ if(mqtt_handle_fd((instance*) fds[n].impl) >= 2){
+ //propagate critical failures
+ return 1;
+ }
+ }
+
+ //keepalive/reconnect processing
+ if(last_maintenance && mm_timestamp() - last_maintenance >= MQTT_KEEPALIVE * 1000){
+ if(mqtt_maintenance()){
+ return 1;
+ }
+ last_maintenance = mm_timestamp();
+ }
+
+ return 0;
+}
+
+static int mqtt_start(size_t n, instance** inst){
+ size_t u = 0, fds = 0;
+
+ for(u = 0; u < n; u++){
+ switch(mqtt_reconnect(inst[u])){
+ case 1:
+ LOGPF("Failed to connect to host for instance %s, will be retried", inst[u]->name);
+ break;
+ case 2:
+ LOGPF("Failed to connect to host for instance %s, aborting", inst[u]->name);
+ return 1;
+ default:
+ fds++;
+ break;
+ }
+ }
+ LOGPF("Registered %" PRIsize_t " descriptors to core", fds);
+
+ //initialize maintenance timer
+ last_maintenance = mm_timestamp();
+ return 0;
+}
+
+static int mqtt_shutdown(size_t n, instance** inst){
+ size_t u, p, v;
+ mqtt_instance_data* data = NULL;
+
+ for(u = 0; u < n; u++){
+ data = (mqtt_instance_data*) inst[u]->impl;
+ mqtt_disconnect(inst[u]);
+
+ for(p = 0; p < data->nchannels; p++){
+ for(v = 0; v < data->channel[p].values; v++){
+ free(data->channel[p].value[v].discrete);
+ }
+ free(data->channel[p].value);
+ free(data->channel[p].topic);
+ }
+ free(data->channel);
+ free(data->host);
+ free(data->port);
+ free(data->user);
+ free(data->password);
+ free(data->client_id);
+
+ free(inst[u]->impl);
+ inst[u]->impl = NULL;
+ }
+
+ LOG("Backend shut down");
+ return 0;
+}
diff --git a/backends/mqtt.h b/backends/mqtt.h
new file mode 100644
index 0000000..c684f99
--- /dev/null
+++ b/backends/mqtt.h
@@ -0,0 +1,87 @@
+#include "midimonster.h"
+
+MM_PLUGIN_API int init();
+static int mqtt_configure(char* option, char* value);
+static int mqtt_configure_instance(instance* inst, char* option, char* value);
+static int mqtt_instance(instance* inst);
+static channel* mqtt_channel(instance* inst, char* spec, uint8_t flags);
+static int mqtt_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int mqtt_handle(size_t num, managed_fd* fds);
+static int mqtt_start(size_t n, instance** inst);
+static int mqtt_shutdown(size_t n, instance** inst);
+
+#define MQTT_PORT "1883"
+#define MQTT_TLS_PORT "8883"
+#define MQTT_BUFFER_LENGTH 8192
+#define MQTT_KEEPALIVE 10
+#define MQTT_VERSION_DEFAULT 0x05
+
+#define MQTT5_NO_LOCAL 0x04
+
+enum /*_mqtt_property_storage_classes*/ {
+ STORAGE_U8,
+ STORAGE_U16,
+ STORAGE_U32,
+ STORAGE_VARINT,
+ STORAGE_PREFIXED,
+ STORAGE_PREFIXPAIR
+};
+
+enum {
+ MSG_RESERVED = 0x00,
+ MSG_CONNECT = 0x10,
+ MSG_CONNACK = 0x20,
+ MSG_PUBLISH = 0x30,
+ MSG_PUBACK = 0x40,
+ MSG_PUBREC = 0x50,
+ MSG_PUBREL = 0x60,
+ MSG_PUBCOMP = 0x70,
+ MSG_SUBSCRIBE = 0x82,
+ MSG_SUBACK = 0x90,
+ MSG_UNSUBSCRIBE = 0xA0,
+ MSG_UNSUBACK = 0xB0,
+ MSG_PINGREQ = 0xC0,
+ MSG_PINGRESP = 0xD0,
+ MSG_DISCONNECT = 0xE0,
+ MSG_AUTH = 0xF0
+};
+
+typedef struct /*_mqtt_value_mapping*/ {
+ double min;
+ double max;
+ double normal;
+ char* discrete;
+} mqtt_channel_value;
+
+typedef struct /*_mqtt_channel*/ {
+ char* topic;
+ uint16_t topic_alias_sent;
+ uint16_t topic_alias_rcvd;
+ uint8_t flags;
+
+ size_t values;
+ mqtt_channel_value* value;
+} mqtt_channel_data;
+
+typedef struct /*_mqtt_instance_data*/ {
+ uint8_t tls;
+ char* host;
+ char* port;
+ uint8_t mqtt_version;
+
+ char* user;
+ char* password;
+ char* client_id;
+
+ size_t nchannels;
+ mqtt_channel_data* channel;
+
+ int fd;
+ uint8_t receive_buffer[MQTT_BUFFER_LENGTH];
+ size_t receive_offset;
+
+ uint64_t last_control;
+ uint16_t packet_identifier;
+ uint16_t server_max_alias;
+ uint16_t current_alias;
+} mqtt_instance_data;
diff --git a/backends/mqtt.md b/backends/mqtt.md
new file mode 100644
index 0000000..85784ef
--- /dev/null
+++ b/backends/mqtt.md
@@ -0,0 +1,85 @@
+### The `mqtt` backend
+
+This backend provides input from and output to a message queueing telemetry transport (MQTT)
+broker. The MQTT protocol is used in lightweight sensor/actor applications, a wide selection
+of smart home implementations and as a generic message bus in many other domains.
+
+The backend implements both the older protocol version MQTT v3.1.1 as well as the current specification
+for MQTT v5.0.
+
+#### Global configuration
+
+This backend does not take any global configuration.
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|---------------------------------------|
+| `host` | `mqtt://10.23.23.1` | none | Host or URI of the MQTT broker |
+| `user` | `midimonster` | none | User name for broker authentication |
+| `password` | `mm` | none | Password for broker authentication |
+| `clientid` | `MM-main` | random | MQTT client identifier (generated randomly at start if unset) |
+| `protocol` | `3.1.1` | `5` | MQTT protocol version (`5` or `3.1.1`) to use for the connection |
+
+The `host` option can be specified as an URI of the form `mqtt[s]://[username][:password]@host.domain[:port]`.
+This allows specifying all necessary settings in one configuration option.
+
+#### Data exchange format
+
+The MQTT protocol places very few restrictions on the exchanged data. Thus, it is necessary to specify the input
+and output data formats accepted respectively output by the MIDIMonster.
+
+The basic format, without further channel-specific configuration, is an ASCII/UTF-8 string representing a floating
+point number between `0.0` and `1.0`. The MIDIMonster will read these and use the value as the normalized event value.
+
+Channels may be specified to use a different value range or even freeform discrete values by preconfiguring
+the channels in the instance configuration section. This is done by specifying options of the form
+
+```
+<channel> = range <min> <max>
+<channel> = discrete [!]<min> [!]<max> <value>
+```
+
+Example configurations:
+```
+/a/topic = range -10 10
+/another/topic = discrete !0.0 0.5 off
+/another/topic = discrete 0.5 !1.0 on
+```
+
+Note that there may be only one range configuration per topic, but there may be multiple discrete configurations.
+
+The first channel preconfiguration example will change the channel value scale to values between `-10` and `10`.
+For input channels, this sets the normalization range. The MIDIMonster will normalize the input value according to the scale.
+For output channels, this sets the output scaling factors.
+
+The second and third channel preconfigurations define two discrete values (`on` and `off`) with accompanying normalized
+values. For input channels, the normalized channel value for a discrete input will be the value marked with an exclamation mark `!`.
+For output channels, the output will be the first discrete value for which the range between `<min>` and `<max>` contains
+the normalized channel value.
+
+These examples mean
+* For `/a/topic`, when mapped as input, the input value `5.0` will generate a normalized event value of `0.75`.
+* For `/a/topic`, when mapped as output, a normalized event value `0.25` will generate an output of `-5.0`.
+* For `/another/topic`, when mapped as an input, the input value `off` will generate a normalized event value of `0.0`.
+* For `/another/topic`, when mapped as an output, a normalized event value of `0.75` will generate an output of `on`.
+
+Values above the maximum or below the minimum will be clamped. The MIDIMonster will not output values out of the
+configured bounds.
+
+#### Channel specification
+
+A channel specification may be any MQTT topic designator not containing the wildcard characters `+` and `#`.
+
+Example mapping:
+```
+mq1./midimonster/in > mq2./midimonster/out
+```
+
+#### Known bugs / problems
+
+If the connection to a server is lost, the connection will be retried in approximately 10 seconds.
+If the server rejects the connection with reason code `0x01`, a protocol failure is assumed. If the initial
+connection was made with `MQTT v5.0`, it is retried with the older protocol version `MQTT v3.1.1`.
+
+Support for TLS-secured connections is planned, but not yet implemented.
diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md
index d09d412..af5e811 100644
--- a/backends/openpixelcontrol.md
+++ b/backends/openpixelcontrol.md
@@ -35,12 +35,12 @@ Channels can be specified by their sequential index (one-based).
Example mapping (data from Strip 2 LED 66's green component is mapped to the blue component of LED 2 on strip 1):
```
-strip1.channel6 < strip2.channel200
+op1.strip1.channel6 < op1.strip2.channel200
```
Additionally, channels may be referred to by their color component and LED index:
```
-strip1.blue2 < strip2.green66
+op1.strip1.blue2 < op2.strip2.green66
```
#### Known bugs / problems
diff --git a/backends/osc.c b/backends/osc.c
index 5887a50..e8673bb 100644
--- a/backends/osc.c
+++ b/backends/osc.c
@@ -1,4 +1,5 @@
#define BACKEND_NAME "osc"
+//#define DEBUG
#include <string.h>
#include <ctype.h>
@@ -629,7 +630,7 @@ static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags){
data->channel[u].out = calloc(data->channel[u].params, sizeof(osc_parameter_value));
}
else if(data->patterns){
- LOGPF("No pattern match found for %s", spec);
+ LOGPF("No preconfigured pattern match found for %s", spec);
}
if(!data->channel[u].path
diff --git a/backends/osc.md b/backends/osc.md
index 1446e06..61b3324 100644
--- a/backends/osc.md
+++ b/backends/osc.md
@@ -78,7 +78,7 @@ 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:
+The following types are currently supported by the MIDIMonster:
* **i**: 32-bit signed integer
* **f**: 32-bit IEEE floating point
diff --git a/backends/python.c b/backends/python.c
index bd73a20..9c0caa1 100644
--- a/backends/python.c
+++ b/backends/python.c
@@ -1,4 +1,9 @@
#define BACKEND_NAME "python"
+//#define DEBUG
+
+#ifdef _WIN32
+ #include <direct.h>
+#endif
#define PY_SSIZE_T_CLEAN
#include <string.h>
@@ -34,6 +39,8 @@ MM_PLUGIN_API int init(){
LOG("Failed to register backend");
return 1;
}
+
+ //Py_UnbufferedStdioFlag = 1;
return 0;
}
@@ -112,6 +119,24 @@ static int python_prepend_str(PyObject* list, char* str){
return 0;
}
+static PyObject* mmpy_channels(PyObject* self, PyObject* args){
+ size_t u = 0;
+ PyObject* list = NULL;
+ instance* inst = *((instance**) PyModule_GetState(self));
+ python_instance_data* data = (python_instance_data*) inst->impl;
+
+ if(!last_timestamp){
+ LOG("The channels() API will not return usable results before the configuration has been read completely");
+ }
+
+ list = PyList_New(data->channels);
+ for(u = 0; u < data->channels; u++){
+ PyList_SET_ITEM(list, u, PyUnicode_FromString(data->channel[u].name));
+ }
+
+ return list;
+}
+
static PyObject* mmpy_output(PyObject* self, PyObject* args){
instance* inst = *((instance**) PyModule_GetState(self));
python_instance_data* data = (python_instance_data*) inst->impl;
@@ -141,7 +166,7 @@ static PyObject* mmpy_output(PyObject* self, PyObject* args){
else{
mm_channel_event(mm_channel(inst, u, 0), val);
}
- return 0;
+ break;
}
}
@@ -383,6 +408,7 @@ static int mmpy_exec(PyObject* module) {
PyObject* capsule = PyDict_GetItemString(PyThreadState_GetDict(), MMPY_INSTANCE_KEY);
if(capsule && inst){
*inst = PyCapsule_GetPointer(capsule, NULL);
+ DBGPF("Initializing extension module on instance %s", (*inst)->name);
return 0;
}
@@ -397,6 +423,7 @@ static int python_configure_instance(instance* inst, char* option, char* value){
//load python script
if(!strcmp(option, "module")){
//swap to interpreter
+ //PyThreadState_Swap(data->interpreter);
PyEval_RestoreThread(data->interpreter);
//import the module
module = PyImport_ImportModule(value);
@@ -432,6 +459,7 @@ static PyObject* mmpy_init(){
{"timestamp", mmpy_timestamp, METH_VARARGS, "Get the core timestamp (in milliseconds)"},
{"manage", mmpy_manage_fd, METH_VARARGS, "(Un-)register a socket or file descriptor for notifications"},
{"interval", mmpy_interval, METH_VARARGS, "Register or update an interval handler"},
+ {"channels", mmpy_channels, METH_VARARGS, "List currently registered instance channels"},
{"cleanup_handler", mmpy_cleanup_handler, METH_VARARGS, "Register or update the instances cleanup handler"},
{0}
};
@@ -472,8 +500,10 @@ static int python_instance(instance* inst){
Py_SetProgramName(program_name);
//initialize python
Py_InitializeEx(0);
- //create, acquire and release the GIL
+ #if PY_MINOR_VERSION < 7
+ //in python 3.6 and earlier, this was required to set up the GIL
PyEval_InitThreads();
+ #endif
python_main = PyEval_SaveThread();
}
@@ -698,6 +728,8 @@ static int python_start(size_t n, instance** inst){
//release interpreter
PyEval_ReleaseThread(data->interpreter);
}
+
+ last_timestamp = mm_timestamp();
return 0;
}
diff --git a/backends/python.md b/backends/python.md
index a78d972..1c0c96f 100644
--- a/backends/python.md
+++ b/backends/python.md
@@ -24,6 +24,7 @@ The `midimonster` module provides the following functions:
| `timestamp()` | `print(midimonster.timestamp())` | Get the internal core timestamp (in milliseconds) |
| `interval(function, long)` | `midimonster.interval(toggle, 100)` | Register a function to be called periodically. Interval is specified in milliseconds (accurate to 10msec). Calling `interval` with the same function again updates the interval. Specifying the interval as `0` cancels the interval |
| `manage(function, socket)` | `midimonster.manage(handler, socket)` | Register a (connected/listening) socket to the MIDIMonster core. Calls `function(socket)` when the socket is ready to read. Calling this method with `None` as the function argument unregisters the socket. A socket may only have one associated handler |
+| `channels()` | `midimonster.channels()` | Fetch a list of all currently known channels on the instance. Note that this function only returns useful data after the configuration has been read completely, i.e. any time after initial startup |
| `cleanup_handler(function)` | `midimonster.cleanup_handler(save_all)`| Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. |
When a channel handler executes, calling `midimonster.inputvalue()` for that exact channel returns the previous value,
@@ -78,8 +79,8 @@ The `python` backend does not take any global configuration.
| Option | Example value | Default value | Description |
|-----------------------|-----------------------|-----------------------|-----------------------------------------------|
-| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location |
-| `default-handler` | `mu_handlers.default` | none | Function to be called as handler for all top-level channels (not belonging to a module) |
+| `module` | `my_handlers` | none | Name of the python module to load (normally the name of a`.py` file without the extension) |
+| `default-handler` | `my_handlers.default` | none | Function to be called as handler for all top-level channels (not belonging to a module) |
A single instance may have multiple `module` options specified. This will make all handlers available within their
module namespaces (see the section on channel specification).
diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c
index 7c5aa69..d349e6f 100644
--- a/backends/rtpmidi.c
+++ b/backends/rtpmidi.c
@@ -427,6 +427,12 @@ static char* rtpmidi_type_name(uint8_t type){
return "aftertouch";
case pitchbend:
return "pitch";
+ case program:
+ return "program";
+ case rpn:
+ return "rpn";
+ case nrpn:
+ return "nrpn";
}
return "unknown";
}
@@ -552,7 +558,7 @@ static int rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t contro
memcpy(&dest_addr, &(data->peer[peer].dest), min(sizeof(dest_addr), data->peer[peer].dest_len));
if(control){
- //calculate remote control port from data port
+ //calculate remote control port from data port
((struct sockaddr_in*) &dest_addr)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &dest_addr)->sin_port) - 1);
}
@@ -577,6 +583,13 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value)
LOGPF("Unknown instance mode %s for instance %s", value, inst->name);
return 1;
}
+ else if(!strcmp(option, "epn-tx")){
+ data->epn_tx_short = 0;
+ if(!strcmp(value, "short")){
+ data->epn_tx_short = 1;
+ }
+ return 0;
+ }
else if(!strcmp(option, "ssrc")){
data->ssrc = strtoul(value, NULL, 0);
if(!data->ssrc){
@@ -705,6 +718,14 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){
ident.fields.type = note;
next_token += 4;
}
+ else if(!strncmp(next_token, "rpn", 3)){
+ ident.fields.type = rpn;
+ next_token += 3;
+ }
+ else if(!strncmp(next_token, "nrpn", 4)){
+ ident.fields.type = nrpn;
+ next_token += 4;
+ }
else if(!strncmp(next_token, "pressure", 8)){
ident.fields.type = pressure;
next_token += 8;
@@ -715,6 +736,9 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){
else if(!strncmp(next_token, "aftertouch", 10)){
ident.fields.type = aftertouch;
}
+ else if(!strncmp(next_token, "program", 7)){
+ ident.fields.type = program;
+ }
else{
LOGPF("Unknown control type in spec %s", spec);
return NULL;
@@ -728,12 +752,38 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){
return NULL;
}
+static size_t rtpmidi_push_midi(uint8_t* payload, size_t bytes_left, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){
+ //FIXME this is a bit simplistic but it works for now
+ if(bytes_left < 4){
+ return 0;
+ }
+
+ //encode timestamp
+ payload[0] = 0;
+
+ //encode midi command
+ payload[1] = type | channel;
+ payload[2] = control;
+ payload[3] = value & 0x7F;
+
+ if(type == pitchbend){
+ payload[2] = value & 0x7F;
+ payload[3] = (value >> 7) & 0x7F;
+ }
+ //channel-wides aftertouch and program are only 2 bytes
+ else if(type == aftertouch || type == program){
+ payload[2] = payload[3];
+ return 3;
+ }
+ return 4;
+}
+
static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){
rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
uint8_t frame[RTPMIDI_PACKET_BUFFER] = "";
rtpmidi_header* rtp_header = (rtpmidi_header*) frame;
rtpmidi_command_header* command_header = (rtpmidi_command_header*) (frame + sizeof(rtpmidi_header));
- size_t offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0;
+ size_t command_length = 0, offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0;
uint8_t* payload = frame + offset;
rtpmidi_channel_ident ident;
@@ -752,27 +802,37 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v
for(u = 0; u < num; u++){
ident.label = c[u]->ident;
- //encode timestamp
- payload[0] = 0;
-
- //encode midi command
- payload[1] = ident.fields.type | ident.fields.channel;
- payload[2] = ident.fields.control;
- payload[3] = v[u].normalised * 127.0;
-
- if(ident.fields.type == pitchbend){
- payload[2] = ((int)(v[u].normalised * 16384.0)) & 0x7F;
- payload[3] = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F;
+ switch(ident.fields.type){
+ case rpn:
+ case nrpn:
+ //transmit parameter number
+ command_length = rtpmidi_push_midi(payload + offset, sizeof(frame) - offset, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control >> 7) & 0x7F);
+ command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, (ident.fields.type == rpn) ? 100 : 98, ident.fields.control & 0x7F);
+
+ //transmit parameter value
+ command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) >> 7) & 0x7F);
+ command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 38, ((uint16_t) (v[u].normalised * 16383.0)) & 0x7F);
+
+ if(!data->epn_tx_short){
+ //clear active parameter
+ command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 101, 127);
+ command_length += rtpmidi_push_midi(payload + offset + command_length, sizeof(frame) - offset, cc, ident.fields.channel, 100, 127);
+ }
+ break;
+ case pitchbend:
+ //TODO check whether this works
+ command_length = rtpmidi_push_midi(payload + offset, sizeof(frame) - offset, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 16383.0);
+ break;
+ default:
+ command_length = rtpmidi_push_midi(payload + offset, sizeof(frame) - offset, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0);
}
- //channel-wide aftertouch is only 2 bytes
- else if(ident.fields.type == aftertouch){
- payload[2] = payload[3];
- payload -= 1;
- offset -= 1;
+
+ if(command_length == 0){
+ LOGPF("Transmit buffer size exceeded on %s", inst->name);
+ break;
}
- payload += 4;
- offset += 4;
+ offset += command_length;
}
//update command section length
@@ -784,7 +844,9 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v
for(u = 0; u < data->peers; u++){
if(data->peer[u].active && data->peer[u].connected){
- sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len);
+ if(sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len) <= 0){
+ LOGPF("Failed to transmit to peer: %s", mmbackend_socket_strerror(errno));
+ }
}
}
@@ -924,6 +986,79 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size
return 0;
}
+//this state machine was copied more-or-less verbatim from the alsa midi implementation - fixes there will need to be integrated
+static void rtpmidi_handle_epn(instance* inst, uint8_t chan, uint16_t control, uint16_t value){
+ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl;
+ rtpmidi_channel_ident ident = {
+ .label = 0
+ };
+ channel* changed = NULL;
+ channel_value val;
+
+ //switching between nrpn and rpn clears all valid bits
+ if(((data->epn_status[chan] & EPN_NRPN) && (control == 101 || control == 100))
+ || (!(data->epn_status[chan] & EPN_NRPN) && (control == 99 || control == 98))){
+ data->epn_status[chan] &= ~(EPN_NRPN | EPN_PARAMETER_LO | EPN_PARAMETER_HI);
+ }
+
+ //setting an address always invalidates the value valid bits
+ if(control >= 98 && control <= 101){
+ data->epn_status[chan] &= ~EPN_VALUE_HI;
+ }
+
+ //parameter hi
+ if(control == 101 || control == 99){
+ data->epn_control[chan] &= 0x7F;
+ data->epn_control[chan] |= value << 7;
+ data->epn_status[chan] |= EPN_PARAMETER_HI | ((control == 99) ? EPN_NRPN : 0);
+ if(control == 101 && value == 127){
+ data->epn_status[chan] &= ~EPN_PARAMETER_HI;
+ }
+ }
+
+ //parameter lo
+ if(control == 100 || control == 98){
+ data->epn_control[chan] &= ~0x7F;
+ data->epn_control[chan] |= value & 0x7F;
+ data->epn_status[chan] |= EPN_PARAMETER_LO | ((control == 98) ? EPN_NRPN : 0);
+ if(control == 100 && value == 127){
+ data->epn_status[chan] &= ~EPN_PARAMETER_LO;
+ }
+ }
+
+ //value hi, clears low, mark as update candidate
+ if(control == 6
+ //check if parameter is set before accepting value update
+ && ((data->epn_status[chan] & (EPN_PARAMETER_HI | EPN_PARAMETER_LO)) == (EPN_PARAMETER_HI | EPN_PARAMETER_LO))){
+ data->epn_value[chan] = value << 7;
+ data->epn_status[chan] |= EPN_VALUE_HI;
+ }
+
+ //value lo, flush the value
+ if(control == 38
+ && data->epn_status[chan] & EPN_VALUE_HI){
+ data->epn_value[chan] &= ~0x7F;
+ data->epn_value[chan] |= value & 0x7F;
+ data->epn_status[chan] &= ~EPN_VALUE_HI;
+
+ if(cfg.detect){
+ LOGPF("Incoming EPN data on channel %s.ch%d.%s%d", inst->name, chan, data->epn_status[chan] & EPN_NRPN ? "nrpn" : "rpn", data->epn_control[chan]);
+ }
+
+ //find the updated channel
+ ident.fields.type = data->epn_status[chan] & EPN_NRPN ? nrpn : rpn;
+ ident.fields.channel = chan;
+ ident.fields.control = data->epn_control[chan];
+ val.normalised = (double) data->epn_value[chan] / 16383.0;
+
+ //push the new value
+ changed = mm_channel(inst, ident.label, 0);
+ if(changed){
+ mm_channel_event(changed, val);
+ }
+ }
+}
+
static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){
uint16_t length = 0;
size_t offset = 1, decode_time = 0, command_bytes = 0;
@@ -996,9 +1131,10 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){
ident.fields.channel = midi_status & 0x0F;
//single byte command
- if(ident.fields.type == aftertouch){
+ if(ident.fields.type == aftertouch || ident.fields.type == program){
ident.fields.control = 0;
val.normalised = (double) frame[offset] / 127.0;
+ val.raw.u64 = frame[offset];
offset++;
}
//two-byte command
@@ -1010,17 +1146,20 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){
if(ident.fields.type == pitchbend){
ident.fields.control = 0;
- val.normalised = (double)((frame[offset] << 7) | frame[offset - 1]) / 16384.0;
+ val.normalised = (double)((frame[offset] << 7) | frame[offset - 1]) / 16383.0;
+ val.raw.u64 = (frame[offset] << 7) | frame[offset - 1];
}
else{
ident.fields.control = frame[offset - 1];
val.normalised = (double) frame[offset] / 127.0;
+ val.raw.u64 = frame[offset];
}
//fix-up note off events
if(ident.fields.type == 0x80){
ident.fields.type = note;
val.normalised = 0;
+ val.raw.u64 = 0;
}
offset++;
@@ -1029,8 +1168,18 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){
DBGPF("Decoded command type %02X channel %d control %d value %f",
ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised);
+ //forward EPN CCs to the EPN state machine
+ if(ident.fields.type == cc
+ && ((ident.fields.control <= 101 && ident.fields.control >= 98)
+ || ident.fields.control == 6
+ || ident.fields.control == 38)){
+ rtpmidi_handle_epn(inst, ident.fields.channel, ident.fields.control, val.raw.u64);
+ }
+
if(cfg.detect){
- if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){
+ if(ident.fields.type == pitchbend
+ || ident.fields.type == aftertouch
+ || ident.fields.type == program){
LOGPF("Incoming data on channel %s.ch%d.%s, value %f",
inst->name, ident.fields.channel,
rtpmidi_type_name(ident.fields.type), val.normalised);
@@ -1150,8 +1299,10 @@ static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){
};
//send to ipv4 and ipv6 mcasts
- sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6));
- sendto(cfg.mdns4_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast));
+ if((sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)) != len)
+ | (sendto(cfg.mdns4_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)) != len)){
+ LOG("Failed to transmit mDNS frame");
+ }
return 0;
}
@@ -1180,13 +1331,14 @@ static int rtpmidi_mdns_detach(instance* inst){
}
offset += bytes;
- //TODO length-checks here
- frame[offset++] = strlen(inst->name);
- memcpy(frame + offset, inst->name, strlen(inst->name));
- offset += strlen(inst->name);
+ //calculate maximum permitted instance name length
+ bytes = min(min(strlen(inst->name), sizeof(frame) - offset - 3), 255);
+ frame[offset++] = bytes;
+ memcpy(frame + offset, inst->name, bytes);
+ offset += bytes;
frame[offset++] = 0xC0;
frame[offset++] = sizeof(dns_header);
- rr->data = htobe16(1 + strlen(inst->name) + 2);
+ rr->data = htobe16(1 + bytes + 2);
free(name.name);
return rtpmidi_mdns_broadcast(frame, offset);
diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h
index 7e6eccc..e88530f 100644
--- a/backends/rtpmidi.h
+++ b/backends/rtpmidi.h
@@ -32,13 +32,21 @@ static int rtpmidi_shutdown(size_t n, instance** inst);
#define DNS_OPCODE(a) (((a) & 0x78) >> 3)
#define DNS_RESPONSE(a) ((a) & 0x80)
+#define EPN_NRPN 8
+#define EPN_PARAMETER_HI 4
+#define EPN_PARAMETER_LO 2
+#define EPN_VALUE_HI 1
+
enum /*_rtpmidi_channel_type*/ {
none = 0,
note = 0x90,
- cc = 0xB0,
pressure = 0xA0,
+ cc = 0xB0,
+ program = 0xC0,
aftertouch = 0xD0,
- pitchbend = 0xE0
+ pitchbend = 0xE0,
+ rpn = 0xF1,
+ nrpn = 0xF2
};
typedef enum /*_rtpmidi_instance_mode*/ {
@@ -49,10 +57,10 @@ typedef enum /*_rtpmidi_instance_mode*/ {
typedef union {
struct {
- uint8_t pad[5];
+ uint8_t pad[4];
uint8_t type;
uint8_t channel;
- uint8_t control;
+ uint16_t control;
} fields;
uint64_t label;
} rtpmidi_channel_ident;
@@ -67,7 +75,7 @@ typedef struct /*_rtpmidi_peer*/ {
ssize_t invite; //invite-list index for apple-mode learned peers (used to track ipv6/ipv4 overlapping invitations)
} rtpmidi_peer;
-typedef struct /*_rtmidi_instance_data*/ {
+typedef struct /*_rtpmidi_instance_data*/ {
rtpmidi_instance_mode mode;
int fd;
@@ -79,6 +87,11 @@ typedef struct /*_rtmidi_instance_data*/ {
uint32_t ssrc;
uint16_t sequence;
+ uint8_t epn_tx_short;
+ uint16_t epn_control[16];
+ uint16_t epn_value[16];
+ uint8_t epn_status[16];
+
//apple-midi config
char* accept;
uint64_t last_announce;
diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md
index 82548bf..8014572 100644
--- a/backends/rtpmidi.md
+++ b/backends/rtpmidi.md
@@ -38,6 +38,7 @@ Common instance configuration parameters
| `ssrc` | `0xDEADBEEF` | Randomly generated | 32-bit synchronization source identifier |
| `mode` | `direct` | none | Instance session management mode (`direct` or `apple`) |
| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times. Bypasses session discovery (but still performs session negotiation) |
+| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter. |
`direct` mode instance configuration parameters
@@ -63,16 +64,22 @@ The `rtpmidi` backend supports mapping different MIDI events to MIDIMonster chan
* `pressure` - Note pressure/aftertouch messages
* `aftertouch` - Channel-wide aftertouch messages
* `pitch` - Channel pitchbend messages
+* `program` - Channel program change messages
+* `rpn` - Registered parameter numbers (14-bit extension)
+* `nrpn` - Non-registered parameter numbers (14-bit extension)
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>`.
+The `pitch`, `aftertouch` program messages/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.
+Every MIDI channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using
+the CC controls 101/100/99/98/38/6. Both control types have 14-bit IDs and 14-bit values.
+
Example mappings:
```
@@ -80,6 +87,8 @@ rmidi1.ch0.note9 > rmidi2.channel1.cc4
rmidi1.channel15.pressure1 > rmidi1.channel0.note0
rmidi1.ch1.aftertouch > rmidi2.ch2.cc0
rmidi1.ch0.pitch > rmidi2.ch1.pitch
+rmidi2.ch15.note1 > rmidi2.ch2.program
+rmidi2.ch0.nrpn900 > rmidi1.ch1.rpn1
```
#### Known bugs / problems
@@ -91,6 +100,12 @@ The mDNS and DNS-SD implementations in this backend are extremely terse, to the
specifications in multiple cases. Due to the complexity involved in supporting these protocols, problems
arising from this will be considered a bug only in cases where they hinder normal operation of the backend.
+Extended parameter numbers (EPNs, the `rpn` and `nrpn` control types) will also generate events on the controls (CC 101 through
+98, 38 and 6) that are used as the lower layer transport. When using EPNs, mapping those controls is probably not useful.
+
+EPN control types support only the full 14-bit transfer encoding, not the shorter variant transmitting only the 7
+high-order bits. This may be changed if there is sufficient interest in the functionality.
+
mDNS discovery may announce flawed records when run on a host with multiple active interfaces.
While this backend should be reasonably stable, there may be problematic edge cases simply due to the
diff --git a/backends/sacn.c b/backends/sacn.c
index 0c0fd10..e395ae2 100644
--- a/backends/sacn.c
+++ b/backends/sacn.c
@@ -29,13 +29,15 @@ static struct /*_sacn_global_config*/ {
sacn_fd* fd;
uint64_t last_announce;
uint32_t next_frame;
+ uint8_t detect;
} global_cfg = {
.source_name = "MIDIMonster",
.cid = {'M', 'I', 'D', 'I', 'M', 'o', 'n', 's', 't', 'e', 'r'},
.fds = 0,
.fd = NULL,
.last_announce = 0,
- .next_frame = 0
+ .next_frame = 0,
+ .detect = 0
};
MM_PLUGIN_API int init(){
@@ -130,6 +132,16 @@ static int sacn_configure(char* option, char* value){
global_cfg.cid[u] = (strtoul(next, &next, 0) & 0xFF);
}
}
+ else if(!strcmp(option, "detect")){
+ global_cfg.detect = 0;
+ if(!strcmp(value, "on")){
+ global_cfg.detect = 1;
+ }
+ else if(!strcmp(value, "verbose")){
+ global_cfg.detect = 2;
+ }
+ return 0;
+ }
else if(!strcmp(option, "bind")){
mmbackend_parse_hostspec(value, &host, &port, &next);
@@ -138,8 +150,13 @@ static int sacn_configure(char* option, char* value){
return 1;
}
- if(next && !strncmp(next, "local", 5)){
- flags = mcast_loop;
+ //parse additional socket options
+ if(next){
+ for(next = strtok(next, " "); next; next = strtok(NULL, " ")){
+ if(!strcmp(next, "local")){
+ flags |= mcast_loop;
+ }
+ }
}
if(sacn_listener(host, port ? port : SACN_PORT, flags)){
@@ -368,7 +385,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){
//send packet if required
if(mark){
- //find output instance data
+ //find output control data for the instance
for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){
if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){
break;
@@ -401,6 +418,9 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame
//source filtering
if(inst_data->filter_enabled && memcmp(inst_data->cid_filter, frame->sender_cid, 16)){
+ if(global_cfg.detect > 1){
+ LOGPF("Discarding data for instance %s due to source filter rule", inst->name);
+ }
return 0;
}
@@ -418,11 +438,19 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame
//handle source priority (currently a 1-bit counter)
if(inst_data->data.last_priority > data->priority){
+ if(global_cfg.detect > 1){
+ LOGPF("Ignoring lower-priority (%d) source on %s, current source is %d", data->priority, inst->name, inst_data->data.last_priority);
+ }
inst_data->data.last_priority = data->priority;
return 0;
}
inst_data->data.last_priority = data->priority;
+ if(!inst_data->last_input && global_cfg.detect){
+ LOGPF("Valid data on instance %s (Universe %u): Source name %.*s, priority %d", inst->name, inst_data->uni, 64, data->source_name, data->priority);
+ }
+ inst_data->last_input = mm_timestamp();
+
//read data (except start code), mark changed channels
for(u = 1; u < be16toh(data->channels); u++){
if(IS_ACTIVE(inst_data->data.map[u - 1])
@@ -583,6 +611,10 @@ static int sacn_handle(size_t num, managed_fd* fds){
if(inst && sacn_process_frame(inst, frame, data)){
LOG("Failed to process frame");
}
+ else if(!inst && global_cfg.detect > 1){
+ //this will only happen with unicast input
+ LOGPF("Received data for unconfigured universe %d on descriptor %" PRIsize_t, be16toh(data->universe), ((uint64_t) fds[u].impl) & 0xFFFF);
+ }
}
}
} while(bytes_read > 0);
@@ -604,6 +636,45 @@ static int sacn_handle(size_t num, managed_fd* fds){
return 0;
}
+static int sacn_start_multicast(instance* inst){
+ sacn_instance_data* data = (sacn_instance_data*) inst->impl;
+ struct sockaddr_storage bound_name = {
+ 0
+ };
+ #ifdef _WIN32
+ struct ip_mreq mcast_req = {
+ .imr_interface.s_addr = INADDR_ANY,
+ #else
+ struct ip_mreqn mcast_req = {
+ .imr_address.s_addr = INADDR_ANY,
+ #endif
+ .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xefff0000) | ((uint32_t) data->uni))
+ };
+ socklen_t bound_length = sizeof(bound_name);
+
+ //select the specific interface to join the mcast group on based on the bind address
+ if(getsockname(global_cfg.fd[data->fd_index].fd, (struct sockaddr*) &bound_name, &bound_length)){
+ LOGPF("Failed to read back local bind address on socket %" PRIsize_t, data->fd_index);
+ return 1;
+ }
+ else if(bound_name.ss_family != AF_INET || !((struct sockaddr_in*) &bound_name)->sin_addr.s_addr){
+ LOGPF("Socket %" PRIsize_t " not bound to a specific IPv4 address, joining multicast input group for instance %s (universe %u) on default interface", data->fd_index, inst->name, data->uni);
+ }
+ else{
+ #ifdef _WIN32
+ mcast_req.imr_interface = ((struct sockaddr_in*) &bound_name)->sin_addr;
+ #else
+ mcast_req.imr_address = ((struct sockaddr_in*) &bound_name)->sin_addr;
+ #endif
+ }
+
+ if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){
+ LOGPF("Failed to join Multicast group for universe %u on instance %s: %s", data->uni, inst->name, mmbackend_socket_strerror(errno));
+ }
+
+ return 0;
+}
+
static int sacn_start(size_t n, instance** inst){
size_t u, p;
int rv = 1;
@@ -611,9 +682,6 @@ static int sacn_start(size_t n, instance** inst){
sacn_instance_id id = {
.label = 0
};
- struct ip_mreq mcast_req = {
- .imr_interface = { INADDR_ANY }
- };
struct sockaddr_in* dest_v4 = NULL;
if(!global_cfg.fds){
@@ -641,11 +709,8 @@ static int sacn_start(size_t n, instance** inst){
}
}
- 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, (uint8_t*) &mcast_req, sizeof(mcast_req))){
- LOGPF("Failed to join Multicast group for universe %u on instance %s: %s", data->uni, inst[u]->name, mmbackend_socket_strerror(errno));
- }
+ if(!data->unicast_input && sacn_start_multicast(inst[u])){
+ return 1;
}
if(data->xmit_prio){
@@ -667,7 +732,7 @@ static int sacn_start(size_t n, instance** inst){
dest_v4 = (struct sockaddr_in*) (&data->dest_addr);
dest_v4->sin_family = AF_INET;
dest_v4->sin_port = htobe16(strtoul(SACN_PORT, NULL, 10));
- dest_v4->sin_addr = mcast_req.imr_multiaddr;
+ dest_v4->sin_addr.s_addr = htobe32(((uint32_t) 0xefff0000) | ((uint32_t) data->uni));
}
}
}
diff --git a/backends/sacn.h b/backends/sacn.h
index 4138f45..0f24538 100644
--- a/backends/sacn.h
+++ b/backends/sacn.h
@@ -40,6 +40,7 @@ typedef struct /*_sacn_universe_model*/ {
} sacn_universe;
typedef struct /*_sacn_instance_model*/ {
+ uint64_t last_input;
uint16_t uni;
uint8_t realtime;
uint8_t xmit_prio;
diff --git a/backends/sacn.md b/backends/sacn.md
index 598f430..244b4c4 100644
--- a/backends/sacn.md
+++ b/backends/sacn.md
@@ -11,6 +11,7 @@ containing all write-enabled universes.
| `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 operation. |
+| `detect` | `on`, `verbose` | `off` | Output additional information on received data packets to help with configuring complex scenarios |
The `bind` configuration value can be extended by the keyword `local` to allow software on the
local host to process the sACN output frames from the MIDIMonster (e.g. `bind = 0.0.0.0 5568 local`).
@@ -58,3 +59,7 @@ To use multicast input, all networking hardware in the path must support the IGM
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`.
+
+When using this backend for output with a fast event source, some events may appear to be lost due to the packet output rate limiting
+mandated by the E1.31 specification (Section `6.6.1 Transmission rate`).
+The rate limiter can be disabled on a per-instance basis using the `realtime` option.
diff --git a/backends/visca.c b/backends/visca.c
new file mode 100644
index 0000000..a36b139
--- /dev/null
+++ b/backends/visca.c
@@ -0,0 +1,466 @@
+#define BACKEND_NAME "visca"
+//#define DEBUG
+
+#include <string.h>
+#include <math.h>
+
+#ifndef _WIN32
+ #include <sys/ioctl.h>
+ #include <asm/termbits.h>
+#endif
+
+#include "visca.h"
+#include "libmmbackend.h"
+
+/* TODO
+ * VISCA server
+ * Command output rate limiting / deduplication
+ * Inquiry
+ * Reconnect on connection close
+ * Speed updates should send motor outputs
+ *
+ */
+
+MM_PLUGIN_API int init(){
+ backend ptz = {
+ .name = BACKEND_NAME,
+ .conf = ptz_configure,
+ .create = ptz_instance,
+ .conf_instance = ptz_configure_instance,
+ .channel = ptz_channel,
+ .handle = ptz_set,
+ .process = ptz_handle,
+ .start = ptz_start,
+ .shutdown = ptz_shutdown
+ };
+
+ //register backend
+ if(mm_backend_register(ptz)){
+ LOG("Failed to register backend");
+ return 1;
+ }
+ return 0;
+}
+
+static int ptz_configure(char* option, char* value){
+ LOG("No backend configuration possible");
+ return 1;
+}
+
+static int ptz_configure_instance(instance* inst, char* option, char* value){
+ char* host = NULL, *port = NULL, *options = NULL;
+ ptz_instance_data* data = (ptz_instance_data*) inst->impl;
+ uint8_t mode = 0;
+
+ if(!strcmp(option, "id")){
+ data->cam_address = strtoul(value, NULL, 10);
+ return 0;
+ }
+ else if(!strcmp(option, "connect")){
+ if(data->fd >= 0){
+ LOGPF("Instance %s already connected", inst->name);
+ return 1;
+ }
+
+ mmbackend_parse_hostspec(value, &host, &port, &options);
+ if(!host || !port){
+ LOGPF("Invalid destination address specified for instance %s", inst->name);
+ return 1;
+ }
+
+ if(options && !strcmp(options, "udp")){
+ mode = 1;
+ }
+
+ data->fd = mmbackend_socket(host, port, mode ? SOCK_DGRAM : SOCK_STREAM, 0, 0, 1);
+ if(data->fd < 0){
+ LOGPF("Failed to connect instance %s", inst->name);
+ return 1;
+ }
+ return 0;
+ }
+ else if(!strcmp(option, "device")){
+ if(data->fd >= 0){
+ LOGPF("Instance %s already connected", inst->name);
+ return 1;
+ }
+
+ #ifdef _WIN32
+ LOG("Direct device connections are not possible on Windows");
+ return 1;
+ #else
+
+ struct termios2 device_config;
+
+ options = strchr(value, ' ');
+ if(options){
+ //terminate port name
+ *options = 0;
+ options++;
+ }
+
+ data->fd = open(value, O_RDWR | O_NONBLOCK);
+ if(data->fd < 0){
+ LOGPF("Failed to connect instance %s to device %s", inst->name, value);
+ return 1;
+ }
+ data->direct_device = 1;
+
+ //configure baudrate
+ if(options){
+ //get current port config
+ if(ioctl(data->fd, TCGETS2, &device_config)){
+ LOGPF("Failed to get port configuration data for %s: %s", value, strerror(errno));
+ return 0;
+ }
+
+ device_config.c_cflag &= ~CBAUD;
+ device_config.c_cflag |= BOTHER;
+ device_config.c_ispeed = strtoul(options, NULL, 10);
+ device_config.c_ospeed = strtoul(options, NULL, 10);
+
+ //set updated config
+ if(ioctl(data->fd, TCSETS2, &device_config)){
+ LOGPF("Failed to set port configuration data for %s: %s", value, strerror(errno));
+ }
+ }
+ return 0;
+ #endif
+ }
+ else if(!strcmp(option, "deadzone")){
+ data->deadzone = strtod(value, NULL);
+ return 0;
+ }
+
+ LOGPF("Unknown instance configuration parameter %s for instance %s", option, inst->name);
+ return 1;
+}
+
+static int ptz_instance(instance* inst){
+ ptz_instance_data* data = calloc(1, sizeof(ptz_instance_data));
+ if(!data){
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ data->fd = -1;
+ data->cam_address = 1;
+ //start with maximum speeds
+ data->max_pan = ptz_channels[panspeed].max;
+ data->max_tilt = ptz_channels[tiltspeed].max;
+ //start with a bit of slack/deadzone in relative movement axes
+ data->deadzone = 0.1;
+
+ inst->impl = data;
+ return 0;
+}
+
+static channel* ptz_channel(instance* inst, char* spec, uint8_t flags){
+ uint64_t command = 0;
+
+ if(flags & mmchannel_input){
+ LOG("This backend currently only supports output channels");
+ return NULL;
+ }
+
+ for(command = 0; command < sentinel; command++){
+ if(!strncmp(spec, ptz_channels[command].name, strlen(ptz_channels[command].name))){
+ break;
+ }
+ }
+
+ DBGPF("Matched spec %s as %s", spec, ptz_channels[command].name ? ptz_channels[command].name : "sentinel");
+
+ if(command == sentinel){
+ LOGPF("Unknown channel spec %s", spec);
+ return NULL;
+ }
+
+ //store the memory to be called above the command type
+ if(command == call || command == store){
+ command |= (strtoul(spec + strlen(ptz_channels[command].name), NULL, 10) << 8);
+ }
+
+ //store relative move direction
+ else if(command == relmove){
+ if(!strcmp(spec + strlen(ptz_channels[relmove].name), ".up")
+ || !strcmp(spec + strlen(ptz_channels[relmove].name), ".y")){
+ command |= (rel_up << 8);
+ }
+ else if(!strcmp(spec + strlen(ptz_channels[relmove].name), ".left")
+ || !strcmp(spec + strlen(ptz_channels[relmove].name), ".x")){
+ command |= (rel_left << 8);
+ }
+
+ if(!strcmp(spec + strlen(ptz_channels[relmove].name), ".down")
+ || !strcmp(spec + strlen(ptz_channels[relmove].name), ".y")){
+ command |= (rel_down << 8);
+ }
+ else if(!strcmp(spec + strlen(ptz_channels[relmove].name), ".right")
+ || !strcmp(spec + strlen(ptz_channels[relmove].name), ".x")){
+ command |= (rel_right << 8);
+ }
+
+ if(command >> 8 == 0){
+ LOGPF("Could not parse relative movement command %s", spec);
+ return NULL;
+ }
+ }
+
+ return mm_channel(inst, command, 1);
+}
+
+static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint8_t* msg){
+ ptz_instance_data* data = (ptz_instance_data*) inst->impl;
+
+ if(c->ident == pan){
+ data->x = ((ptz_channels[pan].max - ptz_channels[pan].min) * v->normalised) + ptz_channels[pan].min - ptz_channels[pan].offset;
+ }
+ else{
+ data->y = ((ptz_channels[tilt].max - ptz_channels[tilt].min) * v->normalised) + ptz_channels[tilt].min - ptz_channels[tilt].offset;
+ }
+
+ //absolute movements happen with maximum speed
+ msg[4] = data->max_pan;
+ msg[5] = data->max_tilt;
+
+ //either i'm doing this wrong or visca is just weird.
+ msg[6] = ((data->x & 0xF000) >> 12);
+ msg[7] = ((data->x & 0x0F00) >> 8);
+ msg[8] = ((data->x & 0xF0) >> 4);
+ msg[9] = (data->x & 0x0F);
+
+ msg[10] = ((data->y & 0xF000) >> 12);
+ msg[11] = ((data->y & 0x0F00) >> 8);
+ msg[12] = ((data->y & 0xF0) >> 4);
+ msg[13] = (data->y & 0x0F);
+
+ return ptz_channels[pan].bytes;
+}
+
+static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint8_t* msg){
+ ptz_instance_data* data = (ptz_instance_data*) inst->impl;
+ if(c->ident == panspeed){
+ data->max_pan = ((ptz_channels[panspeed].max - ptz_channels[panspeed].min) * v->normalised) + ptz_channels[panspeed].min - ptz_channels[panspeed].offset;
+ }
+ else{
+ data->max_tilt = ((ptz_channels[tiltspeed].max - ptz_channels[tiltspeed].min) * v->normalised) + ptz_channels[tiltspeed].min - ptz_channels[tiltspeed].offset;
+ }
+
+ return 0;
+}
+
+static size_t ptz_set_relmove(instance* inst, channel* c, channel_value* v, uint8_t* msg){
+ ptz_instance_data* data = (ptz_instance_data*) inst->impl;
+
+ uint8_t direction = c->ident >> 8, movement = data->relative_movement;
+ double speed_factor = v->normalised;
+
+ if(direction == rel_x
+ || direction == rel_y){
+ //select only one move event
+ direction &= (speed_factor > 0.5) ? (rel_up | rel_left) : (rel_down | rel_right);
+
+ //scale event value to full axis
+ speed_factor = fabs((speed_factor - 0.5) * 2);
+
+ //clamp to deadzone
+ speed_factor = (speed_factor < 2 * data->deadzone) ? 0 : speed_factor;
+ }
+
+ //clear modified axis
+ if(direction & rel_x){
+ movement &= ~rel_x;
+ data->factor_tilt = speed_factor;
+ }
+ else{
+ movement &= ~rel_y;
+ data->factor_pan = speed_factor;
+ }
+
+ if(speed_factor){
+ movement |= direction;
+ }
+
+ //only transmit if something actually changed
+ if(!movement && !data->relative_movement){
+ return 0;
+ }
+ data->relative_movement = movement;
+
+ //set stored axis speed
+ msg[4] = data->max_pan * data->factor_pan;
+ msg[5] = data->max_tilt * data->factor_tilt;
+
+ //update motor control from movement data
+ msg[6] |= (movement & (rel_left | rel_right)) >> 2;
+ msg[7] |= movement & (rel_up | rel_down);
+
+ //stop motors if unset
+ msg[6] = msg[6] ? msg[6] : 3;
+ msg[7] = msg[7] ? msg[7] : 3;
+
+ DBGPF("Moving axis %d with factor %f, total movement now %02X, commanding %d / %d, %d / %d",
+ direction, speed_factor, data->relative_movement,
+ msg[6], msg[4], msg[7], msg[5]);
+
+ return ptz_channels[relmove].bytes;
+}
+
+static size_t ptz_set_zoom(instance* inst, channel* c, channel_value* v, uint8_t* msg){
+ uint16_t position = ((ptz_channels[zoom].max - ptz_channels[zoom].min) * v->normalised) + ptz_channels[zoom].min - ptz_channels[zoom].offset;
+ msg[4] = ((position & 0xF000) >> 12);
+ msg[5] = ((position & 0x0F00) >> 8);
+ msg[6] = ((position & 0xF0) >> 4);
+ msg[7] = (position & 0x0F);
+ return ptz_channels[zoom].bytes;
+}
+
+static size_t ptz_set_focus(instance* inst, channel* c, channel_value* v, uint8_t* msg){
+ uint16_t position = ((ptz_channels[focus].max - ptz_channels[focus].min) * v->normalised) + ptz_channels[focus].min - ptz_channels[focus].offset;
+ msg[4] = ((position & 0xF000) >> 12);
+ msg[5] = ((position & 0x0F00) >> 8);
+ msg[6] = ((position & 0xF0) >> 4);
+ msg[7] = (position & 0x0F);
+ return ptz_channels[focus].bytes;
+}
+
+static size_t ptz_set_focus_mode(instance* inst, channel* c, channel_value* v, uint8_t* msg){
+ msg[4] = (v->normalised > 0.9) ? 2 : 3;
+ return ptz_channels[focus_mode].bytes;
+}
+
+static size_t ptz_set_wb_mode(instance* inst, channel* c, channel_value* v, uint8_t* msg){
+ msg[4] = (v->normalised > 0.9) ? 0 : 5;
+ return ptz_channels[wb_mode].bytes;
+}
+
+static size_t ptz_set_wb(instance* inst, channel* c, channel_value* v, uint8_t* msg){
+ uint8_t command = c->ident & 0xFF;
+ uint8_t value = ((ptz_channels[command].max - ptz_channels[command].min) * v->normalised) + ptz_channels[command].min - ptz_channels[command].offset;
+ msg[6] = value >> 4;
+ msg[7] = value & 0x0F;
+ return ptz_channels[command].bytes;
+}
+
+static size_t ptz_set_memory(instance* inst, channel* c, channel_value* v, uint8_t* msg){
+ if(v->normalised < 0.9){
+ return 0;
+ }
+
+ msg[5] = (c->ident >> 8);
+ return ptz_channels[call].bytes;
+}
+
+static size_t ptz_set_memory_store(instance* inst, channel* c, channel_value* v, uint8_t* msg){
+ if(v->normalised < 0.9){
+ return 0;
+ }
+
+ msg[5] = (c->ident >> 8);
+ return ptz_channels[store].bytes;
+}
+
+static int ptz_write_serial(int fd, uint8_t* data, size_t bytes){
+ ssize_t total = 0, sent;
+
+ while(total < bytes){
+ sent = write(fd, data + total, bytes - total);
+ if(sent < 0){
+ LOGPF("Failed to write to serial port: %s", strerror(errno));
+ return 1;
+ }
+ total += sent;
+ }
+
+ return 0;
+}
+
+static int ptz_set(instance* inst, size_t num, channel** c, channel_value* v){
+ ptz_instance_data* data = (ptz_instance_data*) inst->impl;
+ size_t n = 0, bytes = 0;
+ uint8_t tx[VISCA_BUFFER_LENGTH] = "";
+ uint8_t command = 0;
+
+ for(n = 0; n < num; n++){
+ bytes = 0;
+ command = c[n]->ident & 0xFF;
+
+ if(ptz_channels[command].bytes){
+ memcpy(tx, ptz_channels[command].pattern, ptz_channels[command].bytes);
+ //if no handler function set, assume a parameterless command and send verbatim
+ bytes = ptz_channels[command].bytes;
+ }
+ tx[0] = 0x80 | (data->cam_address & 0xF);
+
+ if(ptz_channels[command].set){
+ bytes = ptz_channels[command].set(inst, c[n], v + n, tx);
+ }
+
+ if(data->direct_device && bytes && ptz_write_serial(data->fd, tx, bytes)){
+ LOGPF("Failed to write %s command on instance %s", ptz_channels[command].name, inst->name);
+ }
+ else if(!data->direct_device && bytes && mmbackend_send(data->fd, tx, bytes)){
+ LOGPF("Failed to push %s command on instance %s", ptz_channels[command].name, inst->name);
+ }
+ }
+ return 0;
+}
+
+static int ptz_handle(size_t num, managed_fd* fds){
+ uint8_t recv_buf[VISCA_BUFFER_LENGTH];
+ size_t u;
+ ssize_t bytes_read;
+ instance* inst = NULL;
+
+ //read and ignore any responses for now
+ for(u = 0; u < num; u++){
+ inst = (instance*) fds[u].impl;
+ bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0);
+ if(bytes_read <= 0){
+ LOGPF("Failed to receive on signaled fd for instance %s", inst->name);
+ //TODO handle failure
+ }
+ else{
+ DBGPF("Ignored %" PRIsize_t " incoming bytes for instance %s", bytes_read, inst->name);
+ }
+ }
+
+ return 0;
+}
+
+static int ptz_start(size_t n, instance** inst){
+ size_t u, fds = 0;
+ ptz_instance_data* data = NULL;
+
+ for(u = 0; u < n; u++){
+ data = (ptz_instance_data*) inst[u]->impl;
+ if(data->fd >= 0){
+ if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u])){
+ LOGPF("Failed to register descriptor for instance %s", inst[u]->name);
+ return 1;
+ }
+ fds++;
+ }
+ }
+
+ LOGPF("Registered %" PRIsize_t " descriptors to core", fds);
+ return 0;
+}
+
+static int ptz_shutdown(size_t n, instance** inst){
+ size_t u;
+ ptz_instance_data* data = NULL;
+
+ for(u = 0; u < n; u++){
+ data = (ptz_instance_data*) inst[u]->impl;
+ if(data->fd >= 0){
+ close(data->fd);
+ }
+ free(data);
+ inst[u]->impl = NULL;
+ }
+
+ LOG("Backend shut down");
+ return 0;
+}
diff --git a/backends/visca.h b/backends/visca.h
new file mode 100644
index 0000000..37f21b1
--- /dev/null
+++ b/backends/visca.h
@@ -0,0 +1,94 @@
+#include "midimonster.h"
+
+MM_PLUGIN_API int init();
+static int ptz_configure(char* option, char* value);
+static int ptz_configure_instance(instance* inst, char* option, char* value);
+static int ptz_instance(instance* inst);
+static channel* ptz_channel(instance* inst, char* spec, uint8_t flags);
+static int ptz_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int ptz_handle(size_t num, managed_fd* fds);
+static int ptz_start(size_t n, instance** inst);
+static int ptz_shutdown(size_t n, instance** inst);
+
+#define VISCA_BUFFER_LENGTH 50
+
+enum /*_ptz_relmove_channel */ {
+ rel_up = 1,
+ rel_down = 2,
+ rel_left = 4,
+ rel_right = 8,
+ rel_x = rel_up | rel_down,
+ rel_y = rel_left | rel_right
+};
+
+typedef struct /*_ptz_instance_data*/ {
+ int fd;
+ uint8_t cam_address;
+ uint16_t x;
+ uint16_t y;
+ uint8_t max_pan, max_tilt;
+ double factor_pan, factor_tilt;
+ uint8_t relative_movement;
+ double deadzone;
+ uint8_t direct_device;
+} ptz_instance_data;
+
+enum /*ptz_channels*/ {
+ //channels with a name that includes another channels as prefix
+ //go first so the channel matching logic works
+ panspeed = 0,
+ tiltspeed,
+ pan,
+ tilt,
+ zoom,
+ focus,
+ focus_mode,
+ wb_red,
+ wb_blue,
+ wb_mode,
+ call,
+ store,
+ home,
+ stop,
+ relmove,
+ sentinel
+};
+
+typedef size_t (*ptz_channel_set)(instance*, channel*, channel_value*, uint8_t* msg);
+static size_t ptz_set_pantilt(instance* inst, channel* c, channel_value* v, uint8_t* msg);
+static size_t ptz_set_ptspeed(instance* inst, channel* c, channel_value* v, uint8_t* msg);
+static size_t ptz_set_zoom(instance* inst, channel* c, channel_value* v, uint8_t* msg);
+static size_t ptz_set_focus(instance* inst, channel* c, channel_value* v, uint8_t* msg);
+static size_t ptz_set_focus_mode(instance* inst, channel* c, channel_value* v, uint8_t* msg);
+static size_t ptz_set_wb_mode(instance* inst, channel* c, channel_value* v, uint8_t* msg);
+static size_t ptz_set_wb(instance* inst, channel* c, channel_value* v, uint8_t* msg);
+static size_t ptz_set_memory(instance* inst, channel* c, channel_value* v, uint8_t* msg);
+static size_t ptz_set_memory_store(instance* inst, channel* c, channel_value* v, uint8_t* msg);
+static size_t ptz_set_relmove(instance* inst, channel* c, channel_value* v, uint8_t* msg);
+
+static struct {
+ char* name;
+ size_t bytes;
+ uint8_t pattern[VISCA_BUFFER_LENGTH];
+ size_t min; //channel range = max - min
+ size_t max;
+ size_t offset; //channel value = normalised * range - offset
+ ptz_channel_set set;
+} ptz_channels[] = {
+ [panspeed] = {"panspeed", 0, {0}, 0x01, 0x18, 0, ptz_set_ptspeed},
+ [tiltspeed] = {"tiltspeed", 0, {0}, 0x01, 0x14, 0, ptz_set_ptspeed},
+ [pan] = {"pan", 15, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 0x990 * 2, 0x990, ptz_set_pantilt},
+ [tilt] = {"tilt", 15, {0x80, 0x01, 0x06, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}, 0, 0x510 * 2, 0x510, ptz_set_pantilt},
+ [zoom] = {"zoom", 9, {0x80, 0x01, 0x04, 0x47, 0, 0, 0, 0, 0xFF}, 0, 0x4000, 0, ptz_set_zoom},
+ [focus] = {"focus", 9, {0x80, 0x01, 0x04, 0x48, 0, 0, 0, 0, 0xFF}, 0, 0x4000, 0, ptz_set_focus},
+ [focus_mode] = {"autofocus", 6, {0x80, 0x01, 0x04, 0x38, 0, 0xFF}, 0, 1, 0, ptz_set_focus_mode},
+ [wb_mode] = {"wb.auto", 6, {0x80, 0x01, 0x04, 0x35, 0, 0xFF}, 0, 1, 0, ptz_set_wb_mode},
+ [wb_red] = {"wb.red", 9, {0x80, 0x01, 0x04, 0x43, 0x00, 0x00, 0, 0, 0xFF}, 0, 255, 0, ptz_set_wb},
+ [wb_blue] = {"wb.blue", 9, {0x80, 0x01, 0x04, 0x44, 0x00, 0x00, 0, 0, 0xFF}, 0, 255, 0, ptz_set_wb},
+ [call] = {"memory", 7, {0x80, 0x01, 0x04, 0x3F, 0x02, 0, 0xFF}, 0, 254, 0, ptz_set_memory},
+ [store] = {"store", 7, {0x80, 0x01, 0x04, 0x3F, 0x01, 0, 0xFF}, 0, 254, 0, ptz_set_memory_store},
+ [home] = {"home", 5, {0x80, 0x01, 0x06, 0x04, 0xFF}, 0, 0, 0, NULL},
+ [relmove] = {"move", 9, {0x80, 0x01, 0x06, 0x01, 0, 0, 0, 0, 0xFF}, 0, 1, 0, ptz_set_relmove},
+ [stop] = {"stop", 9, {0x80, 0x01, 0x06, 0x01, 0, 0, 0x03, 0x03, 0xFF}, 0, 0, 0, ptz_set_relmove},
+ [sentinel] = {"SENTINEL"}
+};
diff --git a/backends/visca.md b/backends/visca.md
new file mode 100644
index 0000000..7b1bcc3
--- /dev/null
+++ b/backends/visca.md
@@ -0,0 +1,70 @@
+### The `visca` backend
+
+The `visca` backend provides control of compatible PTZ (Pan, Tilt, Zoom) controllable cameras
+via the network. The VISCA protocol has, with some variations, been implemented by multiple manufacturers
+in their camera equipment. There may be some specific limits on the command set depending on the make
+and model of your equipment.
+
+This backend can connect to both UDP and TCP based camera control interfaces. On Linux, it can also control
+devices attached to a serial/RS485 adapter.
+
+#### Global configuration
+
+The `visca` backend does not take any global configuration.
+
+#### Instance configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|---------------------------------------------------------------|
+| `id` | `5` | `1` | VISCA Camera address (normally 1 for network communication |
+| `connect` | `10.10.10.1 5678` | none | Camera network address and port. Default connection is TCP, when optionally suffixed with the `udp` keyword, connection will be UDP |
+| `device` | `/dev/ttyUSB0 115200` | none | (Linux only) Device node for a serial port adapter connecting to the camera, optionally followed by the baudrate |
+| `deadzone` | `0.1` | `0.1` | Amount of event value variation to be ignored for relative movement commands |
+
+#### Channel specification
+
+Each instance exposes the following channels
+
+* `pan`: Pan axis (absolute)
+* `tilt`: Tilt axis (absolute)
+* `panspeed`: Pan speed
+* `tiltspeed`: Tilt speed
+* `zoom`: Zoom position
+* `focus`: Focus position
+* `autofocus`: Switch between autofocus (events > 0.9) and manual focus drive mode
+* `wb.auto`: Switch between automatic white balance mode (events > 0.9) and manual white balance mode
+* `wb.red`, `wb.blue`: Red/Blue channel white balance gain values
+* `home`: Return to home position
+* `memory<n>`: Call memory <n> (if incoming event value is greater than 0.9)
+* `store<n>`: Store current pan/tilt/zoom setup to memory <n> (if incoming event value is greater than 0.9)
+* `move.left`, `move.right`, `move.up`, `move.down`: Move relative to the current position. Set speed is multiplied by the event value.
+* `move.x`, `move.y`: Move relative to the current position along the specified axis. Set speed is multiplied by the event value scaled to the full range (ie. `0.0` to `0.5` moves in one direction, `0.5` to `1.0` in the other).
+
+
+Example mappings:
+
+```
+control.pan > visca.pan
+control.tilt > visca.tilt
+control.btn1 > visca.memory1
+control.stick_x > visca.move.x
+control.stick_y > visca.move.y
+```
+
+#### Compatability list
+
+| Manufacturer | Exact model(s) tested | Compatible models | Result / Notes |
+|---------------|-------------------------------|-----------------------------------------------|-------------------------------------------------------|
+| ValueHD | VHD-V61 | Probably all ValueHD Visca-capable devices | Everything works except for absolute focus control |
+| PTZOptics | | Probably all of their PTZ cameras | See ValueHD |
+
+#### Known bugs / problems
+
+Value readback / Inquiry is not yet implemented. This backend currently only does output.
+
+Some manufacturers use VISCA, but require special framing for command flow control. This may be implemented
+in the future if there is sufficient interest. Some commands may not work with some manufacturer's cameras due to
+different value ranges or command ordering.
+
+Please file a ticket if you can confirm this backend working/nonworking with a new make or model
+of camera so we can add it to the compatibility list!
diff --git a/backends/wininput.c b/backends/wininput.c
new file mode 100644
index 0000000..1d1c85b
--- /dev/null
+++ b/backends/wininput.c
@@ -0,0 +1,766 @@
+#define BACKEND_NAME "wininput"
+//#define DEBUG
+
+#include <string.h>
+#include "wininput.h"
+
+#include <mmsystem.h>
+
+//TODO check whether feedback elimination is required
+//TODO might want to store virtual desktop extents in request->limit
+
+static key_info keys[] = {
+ {VK_LBUTTON, "lmb", button}, {VK_RBUTTON, "rmb", button}, {VK_MBUTTON, "mmb", button},
+ {VK_XBUTTON1, "xmb1", button}, {VK_XBUTTON2, "xmb2", button},
+ {VK_BACK, "backspace"},
+ {VK_TAB, "tab"},
+ {VK_CLEAR, "clear"},
+ {VK_RETURN, "enter"},
+ {VK_SHIFT, "shift"},
+ {VK_CONTROL, "control"}, {VK_MENU, "alt"},
+ {VK_CAPITAL, "capslock"},
+ {VK_ESCAPE, "escape"},
+ {VK_SPACE, "space"},
+ {VK_PRIOR, "pageup"}, {VK_NEXT, "pagedown"},
+ {VK_END, "end"}, {VK_HOME, "home"},
+ {VK_PAUSE, "pause"}, {VK_NUMLOCK, "numlock"}, {VK_SCROLL, "scrolllock"},
+ {VK_INSERT, "insert"}, {VK_DELETE, "delete"}, {VK_SNAPSHOT, "printscreen"},
+ {VK_LEFT, "left"}, {VK_UP, "up"}, {VK_RIGHT, "right"}, {VK_DOWN, "down"},
+ {VK_SELECT, "select"},
+ {VK_PRINT, "print"},
+ {VK_EXECUTE, "execute"},
+ {VK_HELP, "help"},
+ {VK_APPS, "apps"},
+ {VK_SLEEP, "sleep"},
+ {VK_NUMPAD0, "num0"}, {VK_NUMPAD1, "num1"}, {VK_NUMPAD2, "num2"}, {VK_NUMPAD3, "num3"},
+ {VK_NUMPAD4, "num4"}, {VK_NUMPAD5, "num5"}, {VK_NUMPAD6, "num6"}, {VK_NUMPAD7, "num7"},
+ {VK_NUMPAD8, "num8"}, {VK_NUMPAD9, "num9"}, {VK_MULTIPLY, "multiply"}, {VK_ADD, "plus"},
+ {VK_SEPARATOR, "comma"}, {VK_SUBTRACT, "minus"}, {VK_DECIMAL, "dot"}, {VK_DIVIDE, "divide"},
+ {VK_F1, "f1"}, {VK_F2, "f2"}, {VK_F3, "f3"}, {VK_F4, "f4"}, {VK_F5, "f5"},
+ {VK_F6, "f6"}, {VK_F7, "f7"}, {VK_F8, "f8"}, {VK_F9, "f9"}, {VK_F10, "f10"},
+ {VK_F11, "f11"}, {VK_F12, "f12"}, {VK_F13, "f13"}, {VK_F14, "f14"}, {VK_F15, "f15"},
+ {VK_F16, "f16"}, {VK_F17, "f17"}, {VK_F18, "f18"}, {VK_F19, "f19"}, {VK_F20, "f20"},
+ {VK_F21, "f21"}, {VK_F22, "f22"}, {VK_F23, "f23"}, {VK_F24, "f24"},
+ {VK_LWIN, "lwin"}, {VK_RWIN, "rwin"},
+ {VK_LSHIFT, "lshift"}, {VK_RSHIFT, "rshift"},
+ {VK_LCONTROL, "lctrl"}, {VK_RCONTROL, "rctrl"},
+ {VK_LMENU, "lmenu"}, {VK_RMENU, "rmenu"},
+ {VK_BROWSER_BACK, "previous"}, {VK_BROWSER_FORWARD, "next"}, {VK_BROWSER_REFRESH, "refresh"},
+ {VK_BROWSER_STOP, "stop"}, {VK_BROWSER_SEARCH, "search"}, {VK_BROWSER_FAVORITES, "favorites"},
+ {VK_BROWSER_HOME, "homepage"},
+ {VK_VOLUME_MUTE, "mute"}, {VK_VOLUME_DOWN, "voldown"}, {VK_VOLUME_UP, "volup"},
+ {VK_MEDIA_NEXT_TRACK, "nexttrack"}, {VK_MEDIA_PREV_TRACK, "prevtrack"},
+ {VK_MEDIA_STOP, "stopmedia"}, {VK_MEDIA_PLAY_PAUSE, "togglemedia"},
+ {VK_LAUNCH_MEDIA_SELECT, "mediaselect"},
+ {VK_LAUNCH_MAIL, "mail"}, {VK_LAUNCH_APP1, "app1"}, {VK_LAUNCH_APP2, "app2"},
+ {VK_OEM_PLUS, "plus"}, {VK_OEM_COMMA, "comma"},
+ {VK_OEM_MINUS, "minus"}, {VK_OEM_PERIOD, "period"},
+ {VK_ZOOM, "zoom"}
+};
+
+static struct {
+ int virtual_x, virtual_y, virtual_width, virtual_height;
+ long mouse_x, mouse_y;
+ size_t requests;
+ //sorted in _start
+ wininput_request* request;
+ uint32_t interval;
+ uint64_t wheel, wheel_max, wheel_delta;
+ uint8_t wheel_inverted;
+} cfg = {
+ .requests = 0,
+ .interval = 50,
+ .wheel_max = 0xFFFF,
+ .wheel_delta = 1
+};
+
+MM_PLUGIN_API int init(){
+ backend wininput = {
+ .name = BACKEND_NAME,
+ .conf = wininput_configure,
+ .create = wininput_instance,
+ .interval = wininput_interval,
+ .conf_instance = wininput_configure_instance,
+ .channel = wininput_channel,
+ .handle = wininput_set,
+ .process = wininput_handle,
+ .start = wininput_start,
+ .shutdown = wininput_shutdown
+ };
+
+ if(sizeof(wininput_channel_ident) != sizeof(uint64_t)){
+ LOG("Channel identification union out of bounds");
+ return 1;
+ }
+
+ //register backend
+ if(mm_backend_register(wininput)){
+ LOG("Failed to register backend");
+ return 1;
+ }
+ return 0;
+}
+
+static int request_comparator(const void * raw_a, const void * raw_b){
+ wininput_request* a = (wininput_request*) raw_a, *b = (wininput_request*) raw_b;
+
+ //sort by type first
+ if(a->ident.fields.type != b->ident.fields.type){
+ return a->ident.fields.type - b->ident.fields.type;
+ }
+
+ //joysticks need to be sorted by controller id first so we can query them once
+ if(a->ident.fields.type == joystick){
+ //joystick id is in the upper bits of control and we dont actually care about anything else
+ return a->ident.fields.control - b->ident.fields.control;
+ }
+
+ //the rest doesnt actually need to be sorted at all
+ return 0;
+}
+
+static uint32_t wininput_interval(){
+ return cfg.interval;
+}
+
+static int wininput_configure(char* option, char* value){
+ int64_t parameter = 0;
+ char* next_token = NULL;
+
+ if(!strcmp(option, "interval")){
+ cfg.interval = strtoul(value, NULL, 0);
+ return 0;
+ }
+ else if(!strcmp(option, "wheel")){
+ parameter = strtoll(value, &next_token, 0);
+
+ cfg.wheel_max = parameter;
+ if(parameter < 0){
+ LOG("Inverting mouse wheel data");
+ cfg.wheel_max = -parameter;
+ cfg.wheel_inverted = 1;
+ }
+ else if(!parameter){
+ LOGPF("Invalid mouse wheel configuration %s", value);
+ return 1;
+ }
+
+ if(next_token && *next_token){
+ cfg.wheel = strtoul(next_token, NULL, 0);
+ }
+
+ if(cfg.wheel > cfg.wheel_max){
+ LOG("Mouse wheel initial value out of range");
+ return 1;
+ }
+
+ return 0;
+ }
+ else if(!strcmp(option, "wheeldelta")){
+ cfg.wheel_delta = strtoul(value, NULL, 0);
+ return 0;
+ }
+
+
+ LOGPF("Unknown backend configuration option %s", option);
+ return 1;
+}
+
+static int wininput_configure_instance(instance* inst, char* option, char* value){
+ LOG("The backend does not take any instance configuration");
+ return 0;
+}
+
+static int wininput_instance(instance* inst){
+ return 0;
+}
+
+static int wininput_subscribe(uint64_t ident, channel* chan){
+ size_t u, n;
+
+ //find an existing request
+ for(u = 0; u < cfg.requests; u++){
+ if(cfg.request[u].ident.label == ident){
+ break;
+ }
+ }
+
+ if(u == cfg.requests){
+ //create a new request
+ cfg.request = realloc(cfg.request, (cfg.requests + 1) * sizeof(wininput_request));
+ if(!cfg.request){
+ cfg.requests = 0;
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+
+ cfg.request[u].ident.label = ident;
+ cfg.request[u].channels = 0;
+ cfg.request[u].channel = NULL;
+ cfg.request[u].state = cfg.request[u].min = cfg.request[u].max = 0;
+ cfg.requests++;
+ }
+
+ //check if already in subscriber list
+ for(n = 0; n < cfg.request[u].channels; n++){
+ if(cfg.request[u].channel[n] == chan){
+ return 0;
+ }
+ }
+
+ //add to subscriber list
+ cfg.request[u].channel = realloc(cfg.request[u].channel, (cfg.request[u].channels + 1) * sizeof(channel*));
+ if(!cfg.request[u].channel){
+ cfg.request[u].channels = 0;
+ LOG("Failed to allocate memory");
+ return 1;
+ }
+ cfg.request[u].channel[n] = chan;
+ cfg.request[u].channels++;
+ return 0;
+}
+
+static uint64_t wininput_channel_mouse(instance* inst, char* spec, uint8_t flags){
+ size_t u;
+ wininput_channel_ident ident = {
+ .fields.type = mouse
+ };
+
+ if(!strcmp(spec, "x")){
+ ident.fields.channel = position;
+ }
+ else if(!strcmp(spec, "y")){
+ ident.fields.channel = position;
+ ident.fields.control = 1;
+ }
+ else if(!strcmp(spec, "wheel")){
+ ident.fields.channel = wheel;
+ if(flags & mmchannel_input){
+ LOG("The mouse wheel can only be used as an output channel");
+ return 0;
+ }
+ }
+ else{
+ //check the buttons
+ for(u = 0; u < sizeof(keys) / sizeof(keys[0]); u++){
+ if(keys[u].channel == button && !strcmp(keys[u].name, spec)){
+ DBGPF("Using keymap %" PRIsize_t " (%d) for spec %s", u, keys[u].keycode, spec);
+ ident.fields.channel = button;
+ ident.fields.control = keys[u].keycode;
+ break;
+ }
+ }
+
+ if(u == sizeof(keys) / sizeof(keys[0])){
+ LOGPF("Unknown mouse control %s", spec);
+ return 0;
+ }
+ }
+
+ return ident.label;
+}
+
+static uint64_t wininput_channel_key(instance* inst, char* spec, uint8_t flags){
+ size_t u;
+ uint16_t scancode = 0;
+ wininput_channel_ident ident = {
+ .fields.type = keyboard,
+ .fields.channel = keypress
+ };
+
+ for(u = 0; u < sizeof(keys) / sizeof(keys[0]); u++){
+ if(keys[u].channel == keypress && !strcmp(keys[u].name, spec)){
+ DBGPF("Using keymap %" PRIsize_t " (%d) for spec %s", u, keys[u].keycode, spec);
+ ident.fields.control = keys[u].keycode;
+ return ident.label;
+ }
+ }
+
+ //no entry in translation table
+ if(strlen(spec) == 1){
+ //try to translate
+ scancode = VkKeyScan(spec[0]);
+ if(scancode != 0x7f7f){
+ DBGPF("Using keyscan result %02X (via %04X) for spec %s", scancode & 0xFF, scancode, spec);
+ ident.fields.control = scancode & 0xFF;
+ return ident.label;
+ }
+ }
+ else if(strlen(spec) > 1){
+ //try to use as literal
+ scancode = strtoul(spec, NULL, 0);
+ if(scancode){
+ DBGPF("Using direct conversion %d for spec %s", scancode & 0xFF, spec);
+ ident.fields.control = scancode & 0xFF;
+ return ident.label;
+ }
+ }
+
+ LOGPF("Unknown keyboard control %s", spec);
+ return 0;
+}
+
+static uint64_t wininput_channel_joystick(instance* inst, char* spec, uint8_t flags){
+ char* token = NULL, *axes = "xyzruvp";
+ uint16_t controller = strtoul(spec, &token, 0);
+ wininput_channel_ident ident = {
+ .fields.type = joystick
+ };
+
+ if(flags & mmchannel_output){
+ LOG("Joystick channels can only be mapped as inputs on Windows");
+ return 0;
+ }
+
+ if(!controller || !token || *token != '.'){
+ LOGPF("Invalid joystick specification %s", spec);
+ return 0;
+ }
+ token++;
+
+ if(strlen(token) == 1 || !strcmp(token, "pov")){
+ if(strchr(axes, token[0])){
+ ident.fields.channel = position;
+ ident.fields.control = ((controller - 1) << 8) | token[0];
+ return ident.label;
+ }
+
+ LOGPF("Unknown joystick axis specification %s", token);
+ return 0;
+ }
+
+ if(!strncmp(token, "button", 6)){
+ ident.fields.control = strtoul(token + 6, NULL, 10);
+ if(!ident.fields.control || ident.fields.control > 32){
+ LOGPF("Button index out of range for specification %s", token);
+ return 0;
+ }
+ ident.fields.channel = button;
+ ident.fields.control |= (controller << 8);
+ return ident.label;
+ }
+
+ LOGPF("Invalid joystick control %s", spec);
+ return 0;
+}
+
+static channel* wininput_channel(instance* inst, char* spec, uint8_t flags){
+ channel* chan = NULL;
+ uint64_t label = 0;
+
+ if(!strncmp(spec, "mouse.", 6)){
+ label = wininput_channel_mouse(inst, spec + 6, flags);
+ }
+ else if(!strncmp(spec, "key.", 4)){
+ label = wininput_channel_key(inst, spec + 4, flags);
+ }
+ else if(!strncmp(spec, "joy", 3)){
+ label = wininput_channel_joystick(inst, spec + 3, flags);
+ }
+ else{
+ LOGPF("Unknown channel spec type %s", spec);
+ }
+
+ if(label){
+ chan = mm_channel(inst, label, 1);
+ if(chan && (flags & mmchannel_input) && wininput_subscribe(label, chan)){
+ return NULL;
+ }
+ return chan;
+ }
+ return NULL;
+}
+
+//for some reason, sendinput only takes "normalized absolute coordinates", which are never again used in the API
+static void wininput_mouse_normalize(long* x, long* y){
+ long normalized_x = (double) (*x - cfg.virtual_x) * (65535.0f / (double) cfg.virtual_width);
+ long normalized_y = (double) (*y - cfg.virtual_y) * (65535.0f / (double) cfg.virtual_height);
+
+ *x = normalized_x;
+ *y = normalized_y;
+}
+
+static INPUT wininput_event_mouse(uint8_t channel, uint8_t control, double value){
+ DWORD flags_down = 0, flags_up = 0;
+ INPUT ev = {
+ .type = INPUT_MOUSE
+ };
+
+ if(channel == position){
+ if(control){
+ cfg.mouse_y = value * 0xFFFF;
+ }
+ else{
+ cfg.mouse_x = value * 0xFFFF;
+ }
+
+ ev.mi.dwFlags |= MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
+ ev.mi.dx = cfg.mouse_x;
+ ev.mi.dy = cfg.mouse_y;
+ }
+ else if(channel == button){
+ switch(control){
+ case VK_LBUTTON:
+ flags_up |= MOUSEEVENTF_LEFTUP;
+ flags_down |= MOUSEEVENTF_LEFTDOWN;
+ break;
+ case VK_RBUTTON:
+ flags_up |= MOUSEEVENTF_RIGHTUP;
+ flags_down |= MOUSEEVENTF_RIGHTDOWN;
+ break;
+ case VK_MBUTTON:
+ flags_up |= MOUSEEVENTF_MIDDLEUP;
+ flags_down |= MOUSEEVENTF_MIDDLEDOWN;
+ break;
+ case VK_XBUTTON1:
+ case VK_XBUTTON2:
+ ev.mi.mouseData = (control == VK_XBUTTON1) ? XBUTTON1 : XBUTTON2;
+ flags_up |= MOUSEEVENTF_XUP;
+ flags_down |= MOUSEEVENTF_XDOWN;
+ break;
+ }
+
+ if(value > 0.9){
+ ev.mi.dwFlags |= flags_down;
+ }
+ else{
+ ev.mi.dwFlags |= flags_up;
+ }
+ }
+ else if(channel == wheel){
+ ev.mi.dwFlags |= MOUSEEVENTF_WHEEL;
+ ev.mi.mouseData = ((value * cfg.wheel_max) - cfg.wheel) * cfg.wheel_delta;
+ if(cfg.wheel_inverted){
+ ev.mi.mouseData *= -1;
+ }
+ DBGPF("Moving wheel %d (invert %d) with delta %d: %d", (value * cfg.wheel_max) - cfg.wheel, cfg.wheel_inverted, cfg.wheel_delta, ev.mi.mouseData);
+ cfg.wheel = (value * cfg.wheel_max);
+ }
+
+ return ev;
+}
+
+static INPUT wininput_event_keyboard(uint8_t channel, uint8_t control, double value){
+ INPUT ev = {
+ .type = INPUT_KEYBOARD
+ };
+
+ if(channel == keypress){
+ ev.ki.wVk = control;
+ if(value < 0.9){
+ ev.ki.dwFlags |= KEYEVENTF_KEYUP;
+ }
+ }
+
+ return ev;
+}
+
+static int wininput_set(instance* inst, size_t num, channel** c, channel_value* v){
+ wininput_channel_ident ident = {
+ .label = 0
+ };
+ size_t n = 0, offset = 0;
+ INPUT events[500];
+
+ if(num > sizeof(events) / sizeof(events[0])){
+ LOGPF("Truncating output on %s to the last %" PRIsize_t " events, please notify the developers", inst->name, sizeof(events) / sizeof(events[0]));
+ offset = num - sizeof(events) / sizeof(events[0]);
+ }
+
+ for(n = 0; n + offset < num; n++){
+ ident.label = c[n + offset]->ident;
+ if(ident.fields.type == mouse){
+ events[n] = wininput_event_mouse(ident.fields.channel, ident.fields.control, v[n + offset].normalised);
+ }
+ else if(ident.fields.type == keyboard){
+ events[n] = wininput_event_keyboard(ident.fields.channel, ident.fields.control, v[n + offset].normalised);
+ }
+ else{
+ n--;
+ offset++;
+ }
+ }
+
+ if(n){
+ offset = SendInput(n, events, sizeof(INPUT));
+ if(offset != n){
+ LOGPF("Output %" PRIsize_t " of %" PRIsize_t " events on %s", offset, n, inst->name);
+ }
+ }
+ return 0;
+}
+
+static int wininput_handle(size_t num, managed_fd* fds){
+ channel_value val = {
+ .normalised = 0
+ };
+ uint8_t mouse_updated = 0, synthesize_off = 0, push_event = 0, current_joystick = 0;
+ uint16_t key_state = 0;
+ size_t u = 0, n;
+ POINT cursor_position;
+ JOYINFOEX joy_info;
+
+ for(u = 0; u < cfg.requests; u++){
+ synthesize_off = 0;
+ push_event = 0;
+ val.normalised = 0;
+
+ if(cfg.request[u].ident.fields.type == mouse
+ && cfg.request[u].ident.fields.channel == position){
+ if(!mouse_updated){
+ //update mouse coordinates
+ if(!GetCursorPos(&cursor_position)){
+ LOG("Failed to update mouse position");
+ continue;
+ }
+ wininput_mouse_normalize(&cursor_position.x, &cursor_position.y);
+ mouse_updated = 1;
+ if(cfg.mouse_x != cursor_position.x
+ || cfg.mouse_y != cursor_position.y){
+ cfg.mouse_x = cursor_position.x;
+ cfg.mouse_y = cursor_position.y;
+ mouse_updated = 2;
+ }
+ }
+
+ val.normalised = (double) cfg.mouse_x / (double) 0xFFFF;
+ if(cfg.request[u].ident.fields.control){
+ val.normalised = (double) cfg.mouse_y / (double) 0xFFFF;
+ }
+
+ if(mouse_updated == 2){
+ push_event = 1;
+ }
+ }
+ else if(cfg.request[u].ident.fields.type == mouse
+ && cfg.request[u].ident.fields.channel == wheel){
+ //ignore wheel requests, can't read that
+ }
+ else if(cfg.request[u].ident.fields.type == keyboard
+ || cfg.request[u].ident.fields.type == mouse){
+ //check key state
+ key_state = GetAsyncKeyState(cfg.request[u].ident.fields.control);
+ if(key_state == 1){
+ //pressed and released?
+ synthesize_off = 1;
+ }
+ if((key_state & ~1) != cfg.request[u].state){
+ //key state changed
+ if(key_state){
+ val.normalised = 1.0;
+ }
+ cfg.request[u].state = key_state & ~1;
+ push_event = 1;
+ }
+ }
+ else if(cfg.request[u].ident.fields.type == joystick){
+ if(cfg.request[u].ident.fields.control >> 8 != current_joystick){
+ joy_info.dwSize = sizeof(joy_info);
+ joy_info.dwFlags = JOY_RETURNALL | JOY_RETURNPOVCTS;
+ if(joyGetPosEx((cfg.request[u].ident.fields.control >> 8) - 1, &joy_info) != JOYERR_NOERROR){
+ LOGPF("Failed to query joystick %d", cfg.request[u].ident.fields.control >> 8);
+ //early exit because other joystick probably won't be connected either (though this may be wrong)
+ //else we would need to think of a way to mark the data invalid for subsequent requests on the same joystick
+ return 0;
+ }
+ current_joystick = cfg.request[u].ident.fields.control >> 8;
+ }
+
+ if(cfg.request[u].ident.fields.channel == button){
+ //button query
+ if(joy_info.dwFlags & JOY_RETURNBUTTONS){
+ key_state = (joy_info.dwButtons & (1 << ((cfg.request[u].ident.fields.control & 0xFF) - 1))) > 0 ? 1 : 0;
+ if(key_state != cfg.request[u].state){
+ if(key_state){
+ val.normalised = 1.0;
+ }
+ cfg.request[u].state = key_state;
+ push_event = 1;
+ DBGPF("Joystick %d button %d: %d",
+ cfg.request[u].ident.fields.control >> 8,
+ cfg.request[u].ident.fields.control & 0xFF,
+ key_state);
+ }
+ }
+ else{
+ LOGPF("No button data received for joystick %d", cfg.request[u].ident.fields.control >> 8);
+ }
+ }
+ else{
+ if(!cfg.request[u].max){
+ cfg.request[u].max = 0xFFFF;
+ }
+ val.raw.u64 = cfg.request[u].state;
+
+ //axis requests, every single access to these structures is stupid.
+ switch(cfg.request[u].ident.fields.control & 0xFF){
+ case 'x':
+ if(joy_info.dwFlags & JOY_RETURNX){
+ val.raw.u64 = joy_info.dwXpos;
+ }
+ break;
+ case 'y':
+ if(joy_info.dwFlags & JOY_RETURNY){
+ val.raw.u64 = joy_info.dwYpos;
+ }
+ break;
+ case 'z':
+ if(joy_info.dwFlags & JOY_RETURNZ){
+ val.raw.u64 = joy_info.dwZpos;
+ }
+ break;
+ case 'r':
+ if(joy_info.dwFlags & JOY_RETURNR){
+ val.raw.u64 = joy_info.dwRpos;
+ }
+ break;
+ case 'u':
+ if(joy_info.dwFlags & JOY_RETURNU){
+ val.raw.u64 = joy_info.dwUpos;
+ }
+ break;
+ case 'v':
+ if(joy_info.dwFlags & JOY_RETURNV){
+ val.raw.u64 = joy_info.dwVpos;
+ }
+ break;
+ case 'p':
+ if(joy_info.dwFlags & (JOY_RETURNPOV | JOY_RETURNPOVCTS)){
+ val.raw.u64 = joy_info.dwPOV;
+ }
+ break;
+ }
+
+ if(val.raw.u64 != cfg.request[u].state){
+ val.normalised = (double) (val.raw.u64 - cfg.request[u].min) / (double) (cfg.request[u].max - cfg.request[u].min);
+ cfg.request[u].state = val.raw.u64;
+ push_event = 1;
+ }
+ }
+ }
+
+ if(push_event){
+ //clamp value just to be safe
+ val.normalised = clamp(val.normalised, 1.0, 0.0);
+ //push current value to all channels
+ DBGPF("Pushing event %f on request %" PRIsize_t, val.normalised, u);
+ for(n = 0; n < cfg.request[u].channels; n++){
+ mm_channel_event(cfg.request[u].channel[n], val);
+ }
+
+ if(synthesize_off){
+ val.normalised = 0;
+ //push synthesized value to all channels
+ DBGPF("Synthesizing event %f on request %" PRIsize_t, val.normalised, u);
+ for(n = 0; n < cfg.request[u].channels; n++){
+ mm_channel_event(cfg.request[u].channel[n], val);
+ }
+ }
+ }
+ }
+ return 0;
+}
+
+static void wininput_start_joystick(){
+ size_t u, p;
+ JOYINFOEX joy_info;
+ JOYCAPS joy_caps;
+
+ DBGPF("This system supports a maximum of %u joysticks", joyGetNumDevs());
+ for(u = 0; u < joyGetNumDevs(); u++){
+ joy_info.dwSize = sizeof(joy_info);
+ joy_info.dwFlags = 0;
+ if(joyGetPosEx(u, &joy_info) == JOYERR_NOERROR){
+ if(joyGetDevCaps(u, &joy_caps, sizeof(joy_caps)) == JOYERR_NOERROR){
+ LOGPF("Joystick %" PRIsize_t " (%s) is available for input", u + 1, joy_caps.szPname ? joy_caps.szPname : "unknown model");
+ for(p = 0; p < cfg.requests; p++){
+ if(cfg.request[p].ident.fields.type == joystick
+ && cfg.request[p].ident.fields.channel == position
+ && (cfg.request[p].ident.fields.control >> 8) == u){
+ //this looks really dumb, but the structure is defined in a way that prevents us from doing anything clever here
+ switch(cfg.request[p].ident.fields.control & 0xFF){
+ case 'x':
+ cfg.request[p].min = joy_caps.wXmin;
+ cfg.request[p].max = joy_caps.wXmax;
+ break;
+ case 'y':
+ cfg.request[p].min = joy_caps.wYmin;
+ cfg.request[p].max = joy_caps.wYmax;
+ break;
+ case 'z':
+ cfg.request[p].min = joy_caps.wZmin;
+ cfg.request[p].max = joy_caps.wZmax;
+ break;
+ case 'r':
+ cfg.request[p].min = joy_caps.wRmin;
+ cfg.request[p].max = joy_caps.wRmax;
+ break;
+ case 'u':
+ cfg.request[p].min = joy_caps.wUmin;
+ cfg.request[p].max = joy_caps.wUmax;
+ break;
+ case 'v':
+ cfg.request[p].min = joy_caps.wVmin;
+ cfg.request[p].max = joy_caps.wVmax;
+ break;
+ }
+ DBGPF("Updated limits on request %" PRIsize_t " (%c) to %" PRIu32 " / %" PRIu32, p, cfg.request[p].ident.fields.control & 0xFF, cfg.request[p].min, cfg.request[p].max);
+ }
+ }
+ }
+ else{
+ LOGPF("Joystick %" PRIsize_t " available for input, but no capabilities reported", u + 1);
+ }
+ }
+ }
+}
+
+static int wininput_start(size_t n, instance** inst){
+ POINT cursor_position;
+
+ //if no input requested, don't request polling
+ if(!cfg.requests){
+ cfg.interval = 0;
+ }
+
+ wininput_start_joystick();
+
+ //read virtual desktop extents for later normalization
+ cfg.virtual_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
+ cfg.virtual_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
+ cfg.virtual_x = GetSystemMetrics(SM_XVIRTUALSCREEN);
+ cfg.virtual_y = GetSystemMetrics(SM_YVIRTUALSCREEN);
+ DBGPF("Virtual screen is %dx%d with offset %dx%d", cfg.virtual_width, cfg.virtual_height, cfg.virtual_x, cfg.virtual_y);
+
+ //sort requests to allow querying each joystick only once
+ qsort(cfg.request, cfg.requests, sizeof(wininput_request), request_comparator);
+
+ //initialize mouse position
+ if(!GetCursorPos(&cursor_position)){
+ LOG("Failed to read initial mouse position");
+ return 1;
+ }
+
+ DBGPF("Current mouse coordinates: %dx%d (%04Xx%04X)", cursor_position.x, cursor_position.y, cursor_position.x, cursor_position.y);
+ wininput_mouse_normalize(&cursor_position.x, &cursor_position.y);
+ DBGPF("Current normalized mouse position: %04Xx%04X", cursor_position.x, cursor_position.y);
+ cfg.mouse_x = cursor_position.x;
+ cfg.mouse_y = cursor_position.y;
+
+ DBGPF("Tracking %" PRIsize_t " input requests", cfg.requests);
+ return 0;
+}
+
+static int wininput_shutdown(size_t n, instance** inst){
+ size_t u;
+
+ for(u = 0; u < cfg.requests; u++){
+ free(cfg.request[u].channel);
+ }
+ free(cfg.request);
+ cfg.request = NULL;
+ cfg.requests = 0;
+
+ LOG("Backend shut down");
+ return 0;
+}
diff --git a/backends/wininput.h b/backends/wininput.h
new file mode 100644
index 0000000..0939cc3
--- /dev/null
+++ b/backends/wininput.h
@@ -0,0 +1,54 @@
+#include "midimonster.h"
+
+MM_PLUGIN_API int init();
+static int wininput_configure(char* option, char* value);
+static int wininput_configure_instance(instance* inst, char* option, char* value);
+static int wininput_instance(instance* inst);
+static channel* wininput_channel(instance* inst, char* spec, uint8_t flags);
+static uint32_t wininput_interval();
+static int wininput_set(instance* inst, size_t num, channel** c, channel_value* v);
+static int wininput_handle(size_t num, managed_fd* fds);
+static int wininput_start(size_t n, instance** inst);
+static int wininput_shutdown(size_t n, instance** inst);
+
+enum /*wininput_channel_type*/ {
+ none = 0,
+ mouse,
+ keyboard,
+ joystick
+};
+
+enum /*wininput_control_channel*/ {
+ keypress = 0,
+ button,
+ position,
+ wheel,
+
+ key_unicode
+};
+
+typedef struct /*_wininput_key_info*/ {
+ uint8_t keycode;
+ char* name;
+ uint8_t channel;
+} key_info;
+
+typedef union {
+ struct {
+ uint8_t pad[4];
+ uint8_t type;
+ uint8_t channel;
+ uint16_t control;
+ } fields;
+ uint64_t label;
+} wininput_channel_ident;
+
+typedef struct /*_input_request*/ {
+ wininput_channel_ident ident;
+ size_t channels;
+ channel** channel;
+ uint32_t state;
+
+ //used for jostick axes
+ uint32_t min, max;
+} wininput_request;
diff --git a/backends/wininput.md b/backends/wininput.md
new file mode 100644
index 0000000..797d879
--- /dev/null
+++ b/backends/wininput.md
@@ -0,0 +1,135 @@
+### The `wininput` backend
+
+This backend allows using the mouse and keyboard as input and output channels on a Windows system.
+For example, it can be used to create hotkey-like behaviour (by reading keyboard input) or to control
+a computer remotely.
+
+As Windows merges all keyboard and mouse input into a single data stream, no fine-grained per-device
+access (as is available under Linux) is possible.
+
+#### Global configuration
+
+| Option | Example value | Default value | Description |
+|---------------|-----------------------|-----------------------|---------------------------------------|
+| `interval` | `100` | `50` | Data polling interval in milliseconds. Lower intervals lead to higher CPU load. This value should normally not be changed. |
+| `wheel` | `-4000 2000` | `65535 0` | Mouse wheel range and optional initial value. To invert the mouse wheel control, specify the range as a negative integer. As the mouse wheel is a relative control, we need to specify a range incoming absolute values are mapped to. This can be used control the wheel resolution and travel size. |
+| `wheeldelta` | `20` | `1` | Multiplier for wheel travel |
+
+#### Instance configuration
+
+This backend does not take any instance-specific configuration.
+
+#### Channel specification
+
+The mouse is exposed as two channels for the position (with the origin being the upper-left corner of the desktop)
+
+* `mouse.x`
+* `mouse.y`
+
+as well as one channel per mouse button
+
+* `mouse.lmb`: Left mouse button
+* `mouse.rmb`: Right mouse button
+* `mouse.mmb`: Middle mouse button
+* `mouse.xmb1`: Extra mouse button 1
+* `mouse.xmb2`: Extra mouse button 2
+
+The (vertical) mouse wheel can be controlled from the MIDIMonster using the `mouse.wheel` channel, but it can not be used
+as an input channel due to limitations in the Windows API. All instances share one `wheel` control (see the section on known
+bugs below). The mouse wheel sensitivity can be controlled by adjusting the absolute travel range, its initial value and
+a wheel delta multiplier.
+
+All keyboard keys that have an [assigned virtual keycode](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes)
+are mappable as MIDIMonster channels using the syntax `key.<keyname>`, with *keyname* being one of the following specifiers:
+
+* One of the keynames listed below (e.g., `key.enter`)
+* For "simple" keys (A-z, 0-9, etc), simply the key glyph (e.g. `key.a`)
+* A hexadecimal number specifying the virtual keycode
+
+Keys are pressed once the normalized event value is greater than `0.9`, and released if under that.
+
+The following keynames are defined in an internal mapping table:
+
+| Key name | Description | Key name | Description |
+|-------------------------------|-----------------------|-------------------------------|-----------------------|
+| `backspace` | | `tab` | |
+| `clear` | | `enter` | |
+| `shift` | | `control` | |
+| `alt` | | `capslock` | |
+| `escape` | | `space` | |
+| `pageup`, `pagedown` | | `end` | |
+| `home` | | `pause` | |
+| `numlock` | | `scrolllock` | |
+| `insert` | | `delete` | |
+| `printscreen` | | `up`, `down`, `left`, `right` | |
+| `select` | | `print` | |
+| `execute` | | `help` | |
+| `apps` | | `sleep` | |
+| `num0` - `num9` | | `multiply` | |
+| `plus` | | `comma` | |
+| `minus` | | `dot` | |
+| `divide` | | `f1` - `f24` | |
+| `lwin`, `rwin` | | `lshift`, `rshift` | |
+| `lctrl`, `rctrl` | | `lmenu`, `rmenu` | |
+| `previous`, `next` | Browser controls | `refresh` | Browser controls |
+| `stop` | Browser controls | `search` | Browser controls |
+| `favorites` | Browser controls | `homepage` | Browser controls |
+| `mute` | | `voldown`, `volup` | |
+| `nexttrack`, `prevtrack` | | `stopmedia`, `togglemedia` | |
+| `mediaselect` | | `mail` | |
+| `app1`, `app2` | | `zoom` | |
+
+Example mappings:
+```
+generator.x > wi1.mouse.x
+input.a > wi1.key.a
+input.X > wi1.key.escape
+```
+
+Joystick and gamepad controllers with up to 32 buttons and 6 axes plus POV hat can be mapped as inputs to the
+MIDIMonster. When starting up, the MIDIMonster will output a list of all connected and usable game controllers.
+
+Controllers can be mapped using the syntax
+
+* `joy<n>.<axisname>` for axes, where `<n>` is the ID of the controller and `<axisname>` is one of
+ * `x`, `y`: Main joystick / analog controller axes
+ * `z`: Third axis / joystick rotation
+ * `r`: Fourth axis / Rudder controller / Slider
+ * `u`, `v`: non-specific fifth/sixth axis
+* `joy<n>.button<b>` for buttons, with `<n>` again being the controller ID and `b` being the button number between
+ 1 and 32 (the maximum supported by Windows)
+
+Use the Windows game controller input calibration and configuration tool to identify the axes and button IDs
+relevant to your controller.
+
+For button channels, the channel value will either be `0` or `1.0`, for axis channels it will be the normalized
+value of the axis (with calibration offsets applied), with the exception of the POV axis, where the channel value
+will be in some way correlated with the direction of view.
+
+Example mappings:
+```
+input.joy1.x > movinghead.pan
+input.joy1.y > movinghead.tilt
+input.joy1.button1 > movinghead.dim
+```
+
+#### Known bugs / problems
+
+Joysticks can only be used as input to the MIDIMonster, as Windows does not provide a method to emulate
+Joystick input from user space. This is unlikely to change.
+
+Keyboard and mouse input is subject to UIPI. You can not send input to applications that run at a higher
+privilege level than the MIDIMonster. This limitation is by design and will not change.
+
+Due to inconsistencies in the Windows API, mouse position input and output may differ for the same cursor location.
+This may be correlated with the use and arrangement of multi-monitor desktops. If you encounter problems with either
+receiving or sending mouse positions, please include a description of your monitor alignment in the issue.
+
+Some antivirus applications may detect this backend as problematic because it uses the same system
+interfaces to read keyboard and mouse input as any malicious application would. While it is definitely
+possible to configure the MIDIMonster to do malicious things, the code itself does not log anything.
+You can verify this by reading the backend code yourself.
+
+Since the Windows input system merges all keyboard/mouse input data into one data stream, using multiple
+instances of this backend is not necessary or useful. It is still supported for technical reasons.
+There may be unexpected side effects when mapping the mouse wheel in multiple instances.
diff --git a/backends/winmidi.c b/backends/winmidi.c
index 030062d..649af2e 100644
--- a/backends/winmidi.c
+++ b/backends/winmidi.c
@@ -74,7 +74,7 @@ static int winmidi_configure(char* option, char* value){
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(!strcmp(option, "read") || !strcmp(option, "source")){
if(data->read){
LOGPF("Instance %s already connected to an input device", inst->name);
return 1;
@@ -82,7 +82,7 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value)
data->read = strdup(value);
return 0;
}
- if(!strcmp(option, "write")){
+ else if(!strcmp(option, "write") || !strcmp(option, "target")){
if(data->write){
LOGPF("Instance %s already connected to an output device", inst->name);
return 1;
@@ -90,6 +90,13 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value)
data->write = strdup(value);
return 0;
}
+ else if(!strcmp(option, "epn-tx")){
+ data->epn_tx_short = 0;
+ if(!strcmp(value, "short")){
+ data->epn_tx_short = 1;
+ }
+ return 0;
+ }
LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name);
return 1;
@@ -148,12 +155,23 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){
ident.fields.type = pressure;
next_token += 8;
}
+ else if(!strncmp(next_token, "rpn", 3)){
+ ident.fields.type = rpn;
+ next_token += 3;
+ }
+ else if(!strncmp(next_token, "nrpn", 4)){
+ ident.fields.type = nrpn;
+ next_token += 4;
+ }
else if(!strncmp(next_token, "pitch", 5)){
ident.fields.type = pitchbend;
}
else if(!strncmp(next_token, "aftertouch", 10)){
ident.fields.type = aftertouch;
}
+ else if(!strncmp(next_token, "program", 7)){
+ ident.fields.type = program;
+ }
else{
LOGPF("Unknown control type in %s", spec);
return NULL;
@@ -167,11 +185,7 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){
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
- };
+static void winmidi_tx(HMIDIOUT port, uint8_t type, uint8_t channel, uint8_t control, uint16_t value){
union {
struct {
uint8_t status;
@@ -183,6 +197,28 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v
} output = {
.dword = 0
};
+
+ output.components.status = type | channel;
+ output.components.data1 = control;
+ output.components.data2 = value & 0x7F;
+
+ if(type == pitchbend){
+ output.components.data1 = value & 0x7F;
+ output.components.data2 = (value >> 7) & 0x7F;
+ }
+ else if(type == aftertouch || type == program){
+ output.components.data1 = value;
+ output.components.data2 = 0;
+ }
+
+ midiOutShortMsg(port, output.dword);
+}
+
+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
+ };
size_t u;
if(!data->device_out){
@@ -193,20 +229,29 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v
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;
+ switch(ident.fields.type){
+ case rpn:
+ case nrpn:
+ //transmit parameter number
+ winmidi_tx(data->device_out, cc, ident.fields.channel, (ident.fields.type == rpn) ? 101 : 99, (ident.fields.control >> 7) & 0x7F);
+ winmidi_tx(data->device_out, cc, ident.fields.channel, (ident.fields.type == rpn) ? 100 : 98, ident.fields.control & 0x7F);
+
+ //transmit parameter value
+ winmidi_tx(data->device_out, cc, ident.fields.channel, 6, (((uint16_t) (v[u].normalised * 16383.0)) >> 7) & 0x7F);
+ winmidi_tx(data->device_out, cc, ident.fields.channel, 38, ((uint16_t) (v[u].normalised * 16383.0)) & 0x7F);
+
+ if(!data->epn_tx_short){
+ //clear active parameter
+ winmidi_tx(data->device_out, cc, ident.fields.channel, 101, 127);
+ winmidi_tx(data->device_out, cc, ident.fields.channel, 100, 127);
+ }
+ break;
+ case pitchbend:
+ winmidi_tx(data->device_out, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 16383.0);
+ break;
+ default:
+ winmidi_tx(data->device_out, ident.fields.type, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0);
}
-
- midiOutShortMsg(data->device_out, output.dword);
}
return 0;
@@ -218,12 +263,18 @@ static char* winmidi_type_name(uint8_t typecode){
return "note";
case cc:
return "cc";
+ case rpn:
+ return "rpn";
+ case nrpn:
+ return "nrpn";
case pressure:
return "pressure";
case aftertouch:
return "aftertouch";
case pitchbend:
return "pitch";
+ case program:
+ return "program";
}
return "unknown";
}
@@ -248,7 +299,8 @@ static int winmidi_handle(size_t num, managed_fd* fds){
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){
+ || backend_config.event[u].channel.fields.type == aftertouch
+ || backend_config.event[u].channel.fields.type == program){
LOGPF("Incoming data on channel %s.ch%d.%s, value %f",
backend_config.event[u].inst->name,
backend_config.event[u].channel.fields.channel,
@@ -275,11 +327,98 @@ static int winmidi_handle(size_t num, managed_fd* fds){
return 0;
}
-static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DWORD_PTR inst, DWORD param1, DWORD param2){
+static int winmidi_enqueue_input(instance* inst, winmidi_channel_ident ident, channel_value val){
+ 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){
+ LOG("Failed to allocate memory");
+ backend_config.events_alloc = 0;
+ backend_config.events_active = 0;
+ LeaveCriticalSection(&backend_config.push_events);
+ return 1;
+ }
+ backend_config.events_alloc++;
+ }
+ backend_config.event[backend_config.events_active].inst = 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);
+ return 0;
+}
+
+//this state machine was copied more-or-less verbatim from the alsa midi implementation - fixes there will need to be integrated
+static void winmidi_handle_epn(instance* inst, uint8_t chan, uint16_t control, uint16_t value){
+ winmidi_instance_data* data = (winmidi_instance_data*) inst->impl;
winmidi_channel_ident ident = {
.label = 0
};
channel_value val;
+
+ //switching between nrpn and rpn clears all valid bits
+ if(((data->epn_status[chan] & EPN_NRPN) && (control == 101 || control == 100))
+ || (!(data->epn_status[chan] & EPN_NRPN) && (control == 99 || control == 98))){
+ data->epn_status[chan] &= ~(EPN_NRPN | EPN_PARAMETER_LO | EPN_PARAMETER_HI);
+ }
+
+ //setting an address always invalidates the value valid bits
+ if(control >= 98 && control <= 101){
+ data->epn_status[chan] &= ~EPN_VALUE_HI;
+ }
+
+ //parameter hi
+ if(control == 101 || control == 99){
+ data->epn_control[chan] &= 0x7F;
+ data->epn_control[chan] |= value << 7;
+ data->epn_status[chan] |= EPN_PARAMETER_HI | ((control == 99) ? EPN_NRPN : 0);
+ if(control == 101 && value == 127){
+ data->epn_status[chan] &= ~EPN_PARAMETER_HI;
+ }
+ }
+
+ //parameter lo
+ if(control == 100 || control == 98){
+ data->epn_control[chan] &= ~0x7F;
+ data->epn_control[chan] |= value & 0x7F;
+ data->epn_status[chan] |= EPN_PARAMETER_LO | ((control == 98) ? EPN_NRPN : 0);
+ if(control == 100 && value == 127){
+ data->epn_status[chan] &= ~EPN_PARAMETER_LO;
+ }
+ }
+
+ //value hi, clears low, mark as update candidate
+ if(control == 6
+ //check if parameter is set before accepting value update
+ && ((data->epn_status[chan] & (EPN_PARAMETER_HI | EPN_PARAMETER_LO)) == (EPN_PARAMETER_HI | EPN_PARAMETER_LO))){
+ data->epn_value[chan] = value << 7;
+ data->epn_status[chan] |= EPN_VALUE_HI;
+ }
+
+ //value lo, flush the value
+ if(control == 38
+ && data->epn_status[chan] & EPN_VALUE_HI){
+ data->epn_value[chan] &= ~0x7F;
+ data->epn_value[chan] |= value & 0x7F;
+ data->epn_status[chan] &= ~EPN_VALUE_HI;
+
+ //find the updated channel
+ ident.fields.type = data->epn_status[chan] & EPN_NRPN ? nrpn : rpn;
+ ident.fields.channel = chan;
+ ident.fields.control = data->epn_control[chan];
+ val.normalised = (double) data->epn_value[chan] / 16383.0;
+
+ winmidi_enqueue_input(inst, ident, val);
+ }
+}
+
+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 = {
+ 0
+ };
union {
struct {
uint8_t status;
@@ -305,18 +444,22 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW
ident.fields.type = input.components.status & 0xF0;
ident.fields.control = input.components.data1;
val.normalised = (double) input.components.data2 / 127.0;
+ val.raw.u64 = input.components.data2;
if(ident.fields.type == 0x80){
ident.fields.type = note;
val.normalised = 0;
+ val.raw.u64 = 0;
}
else if(ident.fields.type == pitchbend){
ident.fields.control = 0;
- val.normalised = (double)((input.components.data2 << 7) | input.components.data1) / 16384.0;
+ val.normalised = (double) ((input.components.data2 << 7) | input.components.data1) / 16383.0;
+ val.raw.u64 = input.components.data2 << 7 | input.components.data1;
}
- else if(ident.fields.type == aftertouch){
+ else if(ident.fields.type == aftertouch || ident.fields.type == program){
ident.fields.control = 0;
val.normalised = (double) input.components.data1 / 127.0;
+ val.raw.u64 = input.components.data1;
}
break;
case MIM_LONGDATA:
@@ -332,26 +475,19 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW
return;
}
+ //pass changes in the (n)rpn CCs to the EPN state machine
+ if(ident.fields.type == cc
+ && ((ident.fields.control <= 101 && ident.fields.control >= 98)
+ || ident.fields.control == 6
+ || ident.fields.control == 38)){
+ winmidi_handle_epn((instance*) inst, ident.fields.channel, ident.fields.control, val.raw.u64);
+ }
+
DBGPF("Incoming message type %d channel %d control %d value %f",
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){
- LOG("Failed to allocate memory");
- backend_config.events_alloc = 0;
- backend_config.events_active = 0;
- LeaveCriticalSection(&backend_config.push_events);
- return;
- }
- backend_config.events_alloc++;
+ if(winmidi_enqueue_input((instance*) inst, ident, val)){
+ LOG("Failed to enqueue incoming data");
}
- 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
diff --git a/backends/winmidi.h b/backends/winmidi.h
index 4c740ea..40b3554 100644
--- a/backends/winmidi.h
+++ b/backends/winmidi.h
@@ -10,9 +10,20 @@ static int winmidi_handle(size_t num, managed_fd* fds);
static int winmidi_start(size_t n, instance** inst);
static int winmidi_shutdown(size_t n, instance** inst);
+#define EPN_NRPN 8
+#define EPN_PARAMETER_HI 4
+#define EPN_PARAMETER_LO 2
+#define EPN_VALUE_HI 1
+
typedef struct /*_winmidi_instance_data*/ {
char* read;
char* write;
+
+ uint8_t epn_tx_short;
+ uint16_t epn_control[16];
+ uint16_t epn_value[16];
+ uint8_t epn_status[16];
+
HMIDIIN device_in;
HMIDIOUT device_out;
} winmidi_instance_data;
@@ -20,18 +31,21 @@ typedef struct /*_winmidi_instance_data*/ {
enum /*_winmidi_channel_type*/ {
none = 0,
note = 0x90,
- cc = 0xB0,
pressure = 0xA0,
+ cc = 0xB0,
+ program = 0xC0,
aftertouch = 0xD0,
- pitchbend = 0xE0
+ pitchbend = 0xE0,
+ rpn = 0xF1,
+ nrpn = 0xF2
};
typedef union {
struct {
- uint8_t pad[5];
+ uint8_t pad[4];
uint8_t type;
uint8_t channel;
- uint8_t control;
+ uint16_t control;
} fields;
uint64_t label;
} winmidi_channel_ident;
diff --git a/backends/winmidi.md b/backends/winmidi.md
index 25a6378..9e7d9cc 100644
--- a/backends/winmidi.md
+++ b/backends/winmidi.md
@@ -15,10 +15,11 @@ some deviations may still be present.
#### 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 |
+| Option | Example value | Default value | Description |
+|-----------------------|-----------------------|-----------------------|-----------------------|
+| `read` / `source` | `2` | none | MIDI device to connect for input |
+| `write` / `target` | `DeviceName` | none | MIDI device to connect for output |
+| `epn-tx` | `short` | `full` | Configure whether to clear the active parameter number after transmitting an `nrpn` or `rpn` parameter. |
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.
@@ -32,26 +33,40 @@ The `winmidi` backend supports mapping different MIDI events as MIDIMonster chan
* `pressure` - Note pressure/aftertouch messages
* `aftertouch` - Channel-wide aftertouch messages
* `pitch` - Channel pitchbend messages
+* `program` - Channel program change messages
+* `rpn` - Registered parameter numbers (14-bit extension)
+* `nrpn` - Non-registered parameter numbers (14-bit extension)
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>`.
+The `pitch`, `aftertouch` and `program` messages/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.
+Every MIDI channel also provides `rpn` and `nrpn` controls, which are implemented on top of the MIDI protocol, using
+the CC controls 101/100/99/98/38/6. Both control types have 14-bit IDs and 14-bit values.
+
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
+midi2.ch0.nrpn900 > midi1.ch1.rpn1
+midi2.ch15.note1 > midi1.ch2.program
```
#### Known bugs / problems
+Extended parameter numbers (EPNs, the `rpn` and `nrpn` control types) will also generate events on the controls (CC 101 through
+98, 38 and 6) that are used as the lower layer transport. When using EPNs, mapping those controls is probably not useful.
+
+EPN control types support only the full 14-bit transfer encoding, not the shorter variant transmitting only the 7
+high-order bits. This may be changed if there is sufficient interest in the functionality.
+
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.