From cf889dd8bd983182534457b6778892ef0634e592 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 25 Mar 2018 23:17:15 +0200 Subject: Rough rtpmidi backend skeleton --- backends/Makefile | 2 +- backends/rtpmidi.c | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/rtpmidi.h | 94 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 backends/rtpmidi.c create mode 100644 backends/rtpmidi.h (limited to 'backends') diff --git a/backends/Makefile b/backends/Makefile index 446ad70..771e97e 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,6 +1,6 @@ .PHONY: all clean LINUX_BACKENDS = midi.so evdev.so -BACKENDS = artnet.so osc.so loopback.so sacn.so +BACKENDS = artnet.so osc.so loopback.so sacn.so rtpmidi.so SYSTEM := $(shell uname -s) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c new file mode 100644 index 0000000..f77f93b --- /dev/null +++ b/backends/rtpmidi.c @@ -0,0 +1,130 @@ +#include +#include + +#include "rtpmidi.h" + +#define BACKEND_NAME "rtpmidi" + +/** rtpMIDI backend w/ AppleMIDI support + * + * Global configuration + * bind = 0.0.0.0 + * apple-bind = 0.0.0.1 + * mdns-bind = 0.0.0.0 + * mdns-name = mdns-name + * + * Instance configuration + * interface = 0 + * (opt) ssrc = X + * + * apple-session = session-name + * apple-invite = invite-peer + * apple-allow = * + * or + * connect = + * reply-any = 1 + */ + +static struct /*_rtpmidi_global*/ { + int mdns_fd; + char* mdns_name; + size_t nfds; + rtpmidi_fd* fds; +} cfg = { + .mdns_fd = -1, + .mdns_name = NULL, + .nfds = 0, + .fds = NULL +}; + +int init(){ + backend rtpmidi = { + .name = BACKEND_NAME, + .conf = rtpmidi_configure, + .create = rtpmidi_instance, + .conf_instance = rtpmidi_configure_instance, + .channel = rtpmidi_channel, + .handle = rtpmidi_set, + .process = rtpmidi_handle, + .start = rtpmidi_start, + .shutdown = rtpmidi_shutdown + }; + + if(mm_backend_register(rtpmidi)){ + fprintf(stderr, "Failed to register rtpMIDI backend\n"); + return 1; + } + + return 0; +} + +static int rtpmidi_configure(char* option, char* value){ + if(!strcmp(option, "mdns-name")){ + if(cfg.mdns_name){ + free(cfg.mdns_name); + } + + cfg.mdns_name = strdup(value); + if(!cfg.mdns_name){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + //TODO this should create the mdns broadcaster and responder socket + return 0; + } + else if(!strcmp(option, "bind")){ + //TODO open listening control and data fds + } + + fprintf(stderr, "Unknown rtpMIDI backend option %s\n", option); + return 1; +} + +static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ + //TODO + return 1; +} + +static instance* rtpmidi_instance(){ + //TODO + return NULL; +} + +static channel* rtpmidi_channel(instance* inst, char* spec){ + //TODO + return NULL; +} + +static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ + //TODO + return 1; +} + +static int rtpmidi_handle(size_t num, managed_fd* fds){ + //TODO + return 1; +} + +static int rtpmidi_start(){ + //TODO + return 1; +} + +static int rtpmidi_shutdown(){ + size_t u; + + free(cfg.mdns_name); + if(cfg.mdns_fd >= 0){ + close(cfg.mdns_fd); + } + + for(u = 0; u < cfg.nfds; u++){ + if(cfg.fds[u].data >= 0){ + close(cfg.fds[u].data); + } + if(cfg.fds[u].control >= 0){ + close(cfg.fds[u].control); + } + } + return 0; +} diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h new file mode 100644 index 0000000..f14357f --- /dev/null +++ b/backends/rtpmidi.h @@ -0,0 +1,94 @@ +#include +#include "midimonster.h" + +int init(); +static int rtpmidi_configure(char* option, char* value); +static int rtpmidi_configure_instance(instance* instance, char* option, char* value); +static instance* rtpmidi_instance(); +static channel* rtpmidi_channel(instance* instance, char* spec); +static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); +static int rtpmidi_handle(size_t num, managed_fd* fds); +static int rtpmidi_start(); +static int rtpmidi_shutdown(); + +#define RTPMIDI_DEFAULT_PORTBASE "9001" +#define RTPMIDI_RECV_BUF 4096 + +#define RTPMIDI_COMMAND_LEN +#define RTPMIDI_COMMAND_DATA_OFFSET +#define RTPMIDI_HAS_JOURNAL +#define RTPMIDI_IS_PHANTOM + +typedef enum /*_rtpmidi_peer_mode*/ { + peer_learned, + peer_invited, + peer_invited_by, + peer_sync, + peer_connect +} rtpmidi_peer_mode; + +typedef struct /*_rtpmidi_peer*/ { + rtpmidi_peer_mode mode; + struct sockaddr_storage dest; + socklen_t dest_len; + uint32_t ssrc; +} rtpmidi_peer; + +typedef struct /*_rtpmidi_fd*/ { + int data; + int control; +} rtpmidi_fd; + +typedef struct /*_rtmidi_instance_data*/ { + size_t fd_index; + size_t npeers; + rtpmidi_peer* peers; + uint32_t ssrc; + + //apple-midi config + char* session_name; + char* invite_peers; + char* invite_accept; + + //generic mode config + uint8_t learn_peers; +} rtpmidi_instance_data; + +#pragma pack(push, 1) +typedef struct /*_apple_session_command*/ { + uint16_t res1; + uint8_t command[2]; + uint32_t version; + uint32_t token; + uint32_t ssrc; + //char* name +} apple_command; + +typedef struct /*_apple_session_sync*/ { + uint16_t res1; + uint8_t command[2]; + uint32_t ssrc; + uint8_t count; + uint8_t res2[3]; + uint64_t timestamp[3]; +} apple_sync; + +typedef struct /*_apple_session_feedback*/ { + uint16_t res1; + uint8_t command[2]; + uint32_t ssrc; + uint32_t sequence; +} apple_feedback; + +typedef struct /*_rtp_midi_header*/ { + uint16_t vpxccmpt; //this is really just an amalgamated constant value + uint16_t sequence; + uint32_t timestamp; + uint32_t ssrc; +} rtpmidi_header; + +typedef struct /*_rtp_midi_command*/ { + uint8_t flags; + uint8_t additional_length; +} rtpmidi_command; +#pragma pack(pop) -- cgit v1.2.3 From 72ffbbd002034216f56dc4d00481b2feb61fdbee Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 27 Mar 2018 18:04:32 +0200 Subject: Make rtpmidi backend behave while not providing functionality --- backends/rtpmidi.c | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 165 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index f77f93b..6a8032a 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,11 @@ +#include +#include +#include #include +#include #include +#include +#include #include "rtpmidi.h" @@ -58,10 +64,118 @@ int init(){ return 0; } +static int rtpmidi_listener(char* host, char* port){ + int fd = -1, status, yes = 1, flags; + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_DGRAM, + .ai_flags = AI_PASSIVE + }; + struct addrinfo* info; + struct addrinfo* addr_it; + + status = getaddrinfo(host, port, &hints, &info); + if(status){ + fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); + return -1; + } + + for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ + fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); + if(fd < 0){ + continue; + } + + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); + } + + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); + } + + yes = 0; + if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); + } + + status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } + + break; + } + + freeaddrinfo(info); + + if(!addr_it){ + fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); + return -1; + } + + //set nonblocking + flags = fcntl(fd, F_GETFL, 0); + if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ + fprintf(stderr, "Failed to set rtpMIDI descriptor nonblocking\n"); + close(fd); + return -1; + } + return 0; +} + +static int rtpmidi_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ + struct addrinfo* head; + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_DGRAM + }; + + int error = getaddrinfo(host, port, &hints, &head); + if(error || !head){ + fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); + return 1; + } + + memcpy(addr, head->ai_addr, head->ai_addrlen); + *len = head->ai_addrlen; + + freeaddrinfo(head); + return 0; +} + +static int rtpmidi_separate_hostspec(char* in, char** host, char** port, char* default_port){ + size_t u; + + if(!in || !host || !port){ + return 1; + } + + for(u = 0; in[u] && !isspace(in[u]); u++){ + } + + //guess + *host = in; + + if(in[u]){ + in[u] = 0; + *port = in + u + 1; + } + else{ + //no port given + *port = default_port; + } + return 0; +} + static int rtpmidi_configure(char* option, char* value){ if(!strcmp(option, "mdns-name")){ if(cfg.mdns_name){ - free(cfg.mdns_name); + fprintf(stderr, "Duplicate mdns-name assignment\n"); + return 1; } cfg.mdns_name = strdup(value); @@ -69,11 +183,22 @@ static int rtpmidi_configure(char* option, char* value){ fprintf(stderr, "Failed to allocate memory\n"); return 1; } - //TODO this should create the mdns broadcaster and responder socket return 0; } + else if(!strcmp(option, "mdns-bind")){ + if(cfg.mdns_fd >= 0){ + fprintf(stderr, "Only one mDNS discovery bind is supported\n"); + return 1; + } + + + //TODO create mdns broadcast/responder socket + } else if(!strcmp(option, "bind")){ - //TODO open listening control and data fds + //TODO open listening data fd for raw rtpmidi instance + } + else if(!strcmp(option, "apple-bind")){ + //TODO open control and data fd for applemidi session/rtpmidi+apple-extensions instance } fprintf(stderr, "Unknown rtpMIDI backend option %s\n", option); @@ -101,13 +226,48 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v } static int rtpmidi_handle(size_t num, managed_fd* fds){ + //TODO handle discovery + + if(!num){ + return 0; + } + //TODO return 1; } static int rtpmidi_start(){ - //TODO - return 1; + size_t n, u, p; + int rv = 1; + instance** inst = NULL; + rtpmidi_instance_data* data = NULL; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + if(!n){ + free(inst); + return 0; + } + + if(!cfg.nfds){ + fprintf(stderr, "Failed to start rtpMIDI backend: no descriptors bound\n"); + goto bail; + } + + if(cfg.mdns_fd < 0){ + fprintf(stderr, "No mDNS discovery interface bound, APPLEMIDI session support disabled\n"); + } + + //TODO initialize all instances + + rv = 0; +bail: + free(inst); + return rv; } static int rtpmidi_shutdown(){ -- cgit v1.2.3 From 588f204eaa6c69a8f09a70d9188cefcb6c6075f6 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Dec 2019 02:26:06 +0100 Subject: Partly redesign rtpmidi backend --- backends/rtpmidi.c | 166 ++++++++-------------------------------------------- backends/rtpmidi.h | 10 +--- backends/rtpmidi.md | 85 +++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 147 deletions(-) create mode 100644 backends/rtpmidi.md (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 6a8032a..5387d86 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -11,36 +11,14 @@ #define BACKEND_NAME "rtpmidi" -/** rtpMIDI backend w/ AppleMIDI support - * - * Global configuration - * bind = 0.0.0.0 - * apple-bind = 0.0.0.1 - * mdns-bind = 0.0.0.0 - * mdns-name = mdns-name - * - * Instance configuration - * interface = 0 - * (opt) ssrc = X - * - * apple-session = session-name - * apple-invite = invite-peer - * apple-allow = * - * or - * connect = - * reply-any = 1 - */ - static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; - size_t nfds; - rtpmidi_fd* fds; + uint8_t detect; } cfg = { .mdns_fd = -1, .mdns_name = NULL, - .nfds = 0, - .fds = NULL + .detect = 0 }; int init(){ @@ -57,121 +35,17 @@ int init(){ }; if(mm_backend_register(rtpmidi)){ - fprintf(stderr, "Failed to register rtpMIDI backend\n"); - return 1; - } - - return 0; -} - -static int rtpmidi_listener(char* host, char* port){ - int fd = -1, status, yes = 1, flags; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_DGRAM, - .ai_flags = AI_PASSIVE - }; - struct addrinfo* info; - struct addrinfo* addr_it; - - status = getaddrinfo(host, port, &hints, &info); - if(status){ - fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); - return -1; - } - - for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ - fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); - if(fd < 0){ - continue; - } - - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); - } - - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); - } - - yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); - } - - status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); - if(status < 0){ - close(fd); - continue; - } - - break; - } - - freeaddrinfo(info); - - if(!addr_it){ - fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); - return -1; - } - - //set nonblocking - flags = fcntl(fd, F_GETFL, 0); - if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ - fprintf(stderr, "Failed to set rtpMIDI descriptor nonblocking\n"); - close(fd); - return -1; - } - return 0; -} - -static int rtpmidi_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ - struct addrinfo* head; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_DGRAM - }; - - int error = getaddrinfo(host, port, &hints, &head); - if(error || !head){ - fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); - return 1; - } - - memcpy(addr, head->ai_addr, head->ai_addrlen); - *len = head->ai_addrlen; - - freeaddrinfo(head); - return 0; -} - -static int rtpmidi_separate_hostspec(char* in, char** host, char** port, char* default_port){ - size_t u; - - if(!in || !host || !port){ + fprintf(stderr, "Failed to register rtpmidi backend\n"); return 1; } - for(u = 0; in[u] && !isspace(in[u]); u++){ - } - - //guess - *host = in; - - if(in[u]){ - in[u] = 0; - *port = in + u + 1; - } - else{ - //no port given - *port = default_port; - } return 0; } static int rtpmidi_configure(char* option, char* value){ + char* host = NULL, *port = NULL; + char next_port[10]; + if(!strcmp(option, "mdns-name")){ if(cfg.mdns_name){ fprintf(stderr, "Duplicate mdns-name assignment\n"); @@ -191,17 +65,27 @@ static int rtpmidi_configure(char* option, char* value){ return 1; } + if(mmbackend_parse_hostspec(value, &host, &port)){ + fprintf(stderr, "Not a valid mDNS bind address: %s\n", value); + return 1; + } - //TODO create mdns broadcast/responder socket - } - else if(!strcmp(option, "bind")){ - //TODO open listening data fd for raw rtpmidi instance + cfg.mdns_fd = mmbackend_socket(host, (port ? port : RTPMIDI_MDNS_PORT), SOCK_DGRAM, 1, 1); + if(cfg.mdns_fd < 0){ + fprintf(stderr, "Failed to bind mDNS interface: %s\n", value); + return 1; + } + return 0; } - else if(!strcmp(option, "apple-bind")){ - //TODO open control and data fd for applemidi session/rtpmidi+apple-extensions instance + else if(!strcmp(option, "detect")){ + cfg.detect = 0; + if(!strcmp(value, "on")){ + cfg.detect = 1; + } + return 0; } - fprintf(stderr, "Unknown rtpMIDI backend option %s\n", option); + fprintf(stderr, "Unknown rtpmidi backend option %s\n", option); return 1; } @@ -215,7 +99,7 @@ static instance* rtpmidi_instance(){ return NULL; } -static channel* rtpmidi_channel(instance* inst, char* spec){ +static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ //TODO return NULL; } @@ -242,6 +126,8 @@ static int rtpmidi_start(){ instance** inst = NULL; rtpmidi_instance_data* data = NULL; + //TODO if mdns name defined and no socket, bind default values + //fetch all defined instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index f14357f..6985ede 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -5,7 +5,7 @@ int init(); static int rtpmidi_configure(char* option, char* value); static int rtpmidi_configure_instance(instance* instance, char* option, char* value); static instance* rtpmidi_instance(); -static channel* rtpmidi_channel(instance* instance, char* spec); +static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int rtpmidi_handle(size_t num, managed_fd* fds); static int rtpmidi_start(); @@ -13,11 +13,7 @@ static int rtpmidi_shutdown(); #define RTPMIDI_DEFAULT_PORTBASE "9001" #define RTPMIDI_RECV_BUF 4096 - -#define RTPMIDI_COMMAND_LEN -#define RTPMIDI_COMMAND_DATA_OFFSET -#define RTPMIDI_HAS_JOURNAL -#define RTPMIDI_IS_PHANTOM +#define RTPMIDI_MDNS_PORT "5353" typedef enum /*_rtpmidi_peer_mode*/ { peer_learned, @@ -40,7 +36,7 @@ typedef struct /*_rtpmidi_fd*/ { } rtpmidi_fd; typedef struct /*_rtmidi_instance_data*/ { - size_t fd_index; + int fd; size_t npeers; rtpmidi_peer* peers; uint32_t ssrc; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md new file mode 100644 index 0000000..49c6baf --- /dev/null +++ b/backends/rtpmidi.md @@ -0,0 +1,85 @@ +### The `rtpmidi` backend + +This backend provides read-write access to RTP MIDI streams, which transfer MIDI data +over the network. + +As the specification for RTP MIDI does not normatively indicate any method +for session management, most vendors define their own standards for this. +The MIDIMonster supports the following session management methods, which are +selectable per-instance, with some methods requiring additional global configuration: + +* Direct connection: The instance will send and receive data from peers configured in the + instance configuration +* Direct connection with peer learning: The instance will send and receive data from peers + configured in the instance configuration as well as previously unknown peers that + voluntarily send data to the instance. +* AppleMIDI session management: + +Note that instances that receive data from multiple peers will combine all inputs into one +stream, which may lead to inconsistencies during playback. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration | +| `mdns-bind` | `10.1.2.1 5353` | `0.0.0.0 5353` | Bind host for the mDNS discovery server | +| `mdns-name` | `computer1` | none | mDNS hostname to announce, also used as AppleMIDI peer name | + +#### Instance configuration + +Common instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `ssrc` | `0xDEADBEEF` | Randomly generated | 32-bit synchronization source identifier | +| `mode` | `direct` | none | Instance session management mode (`direct` or `apple`) | + +`direct` mode instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to | +| `learn` | `true` | `false` | Accept new peers for data exchange at runtime | +| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times | + +`apple` mode instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | +| `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | +| `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers) | + +Note that AppleMIDI session establishment requires mDNS functionality, thus the `mdns-name` global parameter +(and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. + +#### Channel specification + +The `rtpmidi` backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are + +* `cc` - Control Changes +* `note` - Note On/Off messages +* `pressure` - Note pressure/aftertouch messages +* `aftertouch` - Channel-wide aftertouch messages +* `pitch` - Channel pitchbend messages + +A MIDIMonster channel is specified using the syntax `channel.`. 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.`. + +MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which +additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called +'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. + +Example mappings: + +``` +rmidi1.ch0.note9 > rmidi2.channel1.cc4 +rmidi1.channel15.pressure1 > rmidi1.channel0.note0 +rmidi1.ch1.aftertouch > rmidi2.ch2.cc0 +rmidi1.ch0.pitch > rmidi2.ch1.pitch +``` + +#### Known bugs / problems -- cgit v1.2.3 From 99d31eabf4a3afa4fd54782cc1d7cd92fbdae084 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Dec 2019 02:27:04 +0100 Subject: Cleanup rtmidi data structures --- backends/rtpmidi.c | 13 ++++--------- backends/rtpmidi.h | 28 ++++++++++++---------------- backends/rtpmidi.md | 5 +++-- 3 files changed, 19 insertions(+), 27 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 5387d86..ea97565 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -7,6 +7,7 @@ #include #include +#include "libmmbackend.h" #include "rtpmidi.h" #define BACKEND_NAME "rtpmidi" @@ -65,7 +66,9 @@ static int rtpmidi_configure(char* option, char* value){ return 1; } - if(mmbackend_parse_hostspec(value, &host, &port)){ + mmbackend_parse_hostspec(value, &host, &port); + + if(!host){ fprintf(stderr, "Not a valid mDNS bind address: %s\n", value); return 1; } @@ -164,13 +167,5 @@ static int rtpmidi_shutdown(){ close(cfg.mdns_fd); } - for(u = 0; u < cfg.nfds; u++){ - if(cfg.fds[u].data >= 0){ - close(cfg.fds[u].data); - } - if(cfg.fds[u].control >= 0){ - close(cfg.fds[u].control); - } - } return 0; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 6985ede..af6a189 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -14,31 +14,27 @@ static int rtpmidi_shutdown(); #define RTPMIDI_DEFAULT_PORTBASE "9001" #define RTPMIDI_RECV_BUF 4096 #define RTPMIDI_MDNS_PORT "5353" +#define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) -typedef enum /*_rtpmidi_peer_mode*/ { - peer_learned, - peer_invited, - peer_invited_by, - peer_sync, - peer_connect -} rtpmidi_peer_mode; +typedef enum /*_rtpmidi_instance_mode*/ { + direct, + apple +} rtpmidi_instance_mode; typedef struct /*_rtpmidi_peer*/ { - rtpmidi_peer_mode mode; struct sockaddr_storage dest; socklen_t dest_len; uint32_t ssrc; } rtpmidi_peer; -typedef struct /*_rtpmidi_fd*/ { - int data; - int control; -} rtpmidi_fd; - typedef struct /*_rtmidi_instance_data*/ { + rtpmidi_instance_mode mode; + int fd; - size_t npeers; - rtpmidi_peer* peers; + int control_fd; + + size_t peers; + rtpmidi_peer* peer; uint32_t ssrc; //apple-midi config @@ -46,7 +42,7 @@ typedef struct /*_rtmidi_instance_data*/ { char* invite_peers; char* invite_accept; - //generic mode config + //direct mode config uint8_t learn_peers; } rtpmidi_instance_data; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 49c6baf..cea69e2 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -49,10 +49,11 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | -| `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers) | +| `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | +| `joint` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | Note that AppleMIDI session establishment requires mDNS functionality, thus the `mdns-name` global parameter -(and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. +(and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. #### Channel specification -- cgit v1.2.3 From aa092469e21674bb99275dda08a695f0d426f6de Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Dec 2019 15:43:05 +0100 Subject: rtpmidi channel spec parsing --- backends/rtpmidi.c | 199 ++++++++++++++++++++++++++++++++++++++++++++++------ backends/rtpmidi.h | 20 ++++++ backends/rtpmidi.md | 2 +- 3 files changed, 200 insertions(+), 21 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index ea97565..4fb0ce0 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -35,6 +35,11 @@ int init(){ .shutdown = rtpmidi_shutdown }; + if(sizeof(rtpmidi_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "rtpmidi channel identification union out of bounds\n"); + return 1; + } + if(mm_backend_register(rtpmidi)){ fprintf(stderr, "Failed to register rtpmidi backend\n"); return 1; @@ -45,7 +50,6 @@ int init(){ static int rtpmidi_configure(char* option, char* value){ char* host = NULL, *port = NULL; - char next_port[10]; if(!strcmp(option, "mdns-name")){ if(cfg.mdns_name){ @@ -93,17 +97,170 @@ static int rtpmidi_configure(char* option, char* value){ } static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ - //TODO + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + + if(!strcmp(option, "mode")){ + if(!strcmp(value, "direct")){ + data->mode = direct; + return 0; + } + else if(!strcmp(value, "apple")){ + data->mode = apple; + return 0; + } + fprintf(stderr, "Unknown rtpmidi instance mode %s for instance %s\n", value, inst->name); + return 1; + } + else if(!strcmp(option, "ssrc")){ + data->ssrc = strtoul(value, NULL, 0); + if(!data->ssrc){ + fprintf(stderr, "Random SSRC will be generated for rtpmidi instance %s\n", inst->name); + } + return 0; + } + else if(!strcmp(option, "bind")){ + //TODO set the bind host + } + else if(!strcmp(option, "learn")){ + if(data->mode != direct){ + fprintf(stderr, "The rtpmidi 'learn' option is only valid for direct mode instances\n"); + return 1; + } + data->learn_peers = 0; + if(!strcmp(value, "true")){ + data->learn_peers = 1; + } + return 0; + } + else if(!strcmp(option, "peer")){ + if(data->mode != direct){ + fprintf(stderr, "The rtpmidi 'peer' option is only valid for direct mode instances\n"); + return 1; + } + + //TODO add peer + return 0; + } + else if(!strcmp(option, "session")){ + if(data->mode != apple){ + fprintf(stderr, "The rtpmidi 'session' option is only valid for apple mode instances\n"); + return 1; + } + free(data->session_name); + data->session_name = strdup(value); + if(!data->session_name){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + return 0; + } + else if(!strcmp(option, "invite")){ + if(data->mode != apple){ + fprintf(stderr, "The rtpmidi 'invite' option is only valid for apple mode instances\n"); + return 1; + } + free(data->invite_peers); + data->invite_peers = strdup(value); + if(!data->invite_peers){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + return 0; + } + else if(!strcmp(option, "join")){ + if(data->mode != apple){ + fprintf(stderr, "The rtpmidi 'join' option is only valid for apple mode instances\n"); + return 1; + } + free(data->invite_accept); + data->invite_accept = strdup(value); + if(!data->invite_accept){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + return 0; + } + + fprintf(stderr, "Unknown rtpmidi instance option %s\n", option); return 1; } static instance* rtpmidi_instance(){ - //TODO - return NULL; + rtpmidi_instance_data* data = NULL; + instance* inst = mm_instance(); + + if(!inst){ + return NULL; + } + + data = calloc(1, sizeof(rtpmidi_instance_data)); + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + inst->impl = data; + return inst; } static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ - //TODO + char* next_token = spec; + rtpmidi_channel_ident ident = { + .label = 0 + }; + + if(!strncmp(spec, "ch", 2)){ + next_token += 2; + if(!strncmp(spec, "channel", 7)){ + next_token = spec + 7; + } + } + else{ + fprintf(stderr, "Invalid rtpmidi channel specification %s\n", spec); + return NULL; + } + + ident.fields.channel = strtoul(next_token, &next_token, 10); + if(ident.fields.channel > 15){ + fprintf(stderr, "rtpmidi channel out of range in channel spec %s\n", spec); + return NULL; + } + + if(*next_token != '.'){ + fprintf(stderr, "rtpmidi channel specification %s does not conform to channel.\n", spec); + return NULL; + } + + next_token++; + + if(!strncmp(next_token, "cc", 2)){ + ident.fields.type = cc; + next_token += 2; + } + else if(!strncmp(next_token, "note", 4)){ + ident.fields.type = note; + next_token += 4; + } + else if(!strncmp(next_token, "pressure", 8)){ + ident.fields.type = pressure; + next_token += 8; + } + else if(!strncmp(next_token, "pitch", 5)){ + ident.fields.type = pitchbend; + } + else if(!strncmp(next_token, "aftertouch", 10)){ + ident.fields.type = aftertouch; + } + else{ + fprintf(stderr, "Unknown rtpmidi channel control type in spec %s\n", spec); + return NULL; + } + + ident.fields.control = strtoul(next_token, NULL, 10); + + if(ident.label){ + return mm_channel(inst, ident.label, 1); + } return NULL; } @@ -124,35 +281,37 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ } static int rtpmidi_start(){ - size_t n, u, p; + size_t n, u; int rv = 1; instance** inst = NULL; rtpmidi_instance_data* data = NULL; //TODO if mdns name defined and no socket, bind default values + if(cfg.mdns_fd < 0){ + fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session support disabled\n"); + } + //fetch all defined instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); return 1; } - if(!n){ - free(inst); - return 0; - } - - if(!cfg.nfds){ - fprintf(stderr, "Failed to start rtpMIDI backend: no descriptors bound\n"); - goto bail; - } + for(u = 0; u < n; u++){ + data = (rtpmidi_instance_data*) inst[u]->impl; + //check whether instances are explicitly configured to a mode + if(data->mode == unconfigured){ + fprintf(stderr, "rtpmidi instance %s is missing a mode configuration\n", inst[u]->name); + goto bail; + } - if(cfg.mdns_fd < 0){ - fprintf(stderr, "No mDNS discovery interface bound, APPLEMIDI session support disabled\n"); + //generate random ssrc's + if(!data->ssrc){ + data->ssrc = rand() << 16 | rand(); + } } - //TODO initialize all instances - rv = 0; bail: free(inst); @@ -160,7 +319,7 @@ bail: } static int rtpmidi_shutdown(){ - size_t u; + //TODO cleanup instance data free(cfg.mdns_name); if(cfg.mdns_fd >= 0){ diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index af6a189..c076fd0 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -16,11 +16,31 @@ static int rtpmidi_shutdown(); #define RTPMIDI_MDNS_PORT "5353" #define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) +enum /*_rtpmidi_channel_type*/ { + none = 0, + note = 0x90, + cc = 0xB0, + pressure = 0xA0, + aftertouch = 0xD0, + pitchbend = 0xE0 +}; + typedef enum /*_rtpmidi_instance_mode*/ { + unconfigured = 0, direct, apple } rtpmidi_instance_mode; +typedef union { + struct { + uint8_t pad[5]; + uint8_t type; + uint8_t channel; + uint8_t control; + } fields; + uint64_t label; +} rtpmidi_channel_ident; + typedef struct /*_rtpmidi_peer*/ { struct sockaddr_storage dest; socklen_t dest_len; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index cea69e2..c84c5b3 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -50,7 +50,7 @@ Common instance configuration parameters | `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | | `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | -| `joint` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | +| `join` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | Note that AppleMIDI session establishment requires mDNS functionality, thus the `mdns-name` global parameter (and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. -- cgit v1.2.3 From 48e12201f5c57cda581bc0c713d99da6524c49a8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 9 Dec 2019 23:14:23 +0100 Subject: rtpmidi socket binding --- backends/rtpmidi.c | 87 ++++++++++++++++++++++++++++++++++++++++++++++++----- backends/rtpmidi.h | 2 ++ backends/rtpmidi.md | 8 ++--- 3 files changed, 85 insertions(+), 12 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 4fb0ce0..38cc9c1 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,6 +1,3 @@ -#include -#include -#include #include #include #include @@ -96,8 +93,40 @@ static int rtpmidi_configure(char* option, char* value){ return 1; } +static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* port){ + struct sockaddr_storage sock_addr = { + 0 + }; + socklen_t sock_len = sizeof(sock_addr); + char control_port[32]; + + //bind to random port if none supplied + data->fd = mmbackend_socket(host, port ? port : "0", SOCK_DGRAM, 1, 0); + if(data->fd < 0){ + return 1; + } + + //bind control port + if(data->mode == apple){ + if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ + fprintf(stderr, "Failed to fetch data port information: %s\n", strerror(errno)); + return 1; + } + + snprintf(control_port, sizeof(control_port), "%d", be16toh(((struct sockaddr_in*)&sock_addr)->sin_port) - 1); + data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0); + if(data->control_fd < 0){ + fprintf(stderr, "Failed to bind control port %s\n", control_port); + return 1; + } + } + + return 0; +} + static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + char* host = NULL, *port = NULL; if(!strcmp(option, "mode")){ if(!strcmp(value, "direct")){ @@ -119,7 +148,19 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 0; } else if(!strcmp(option, "bind")){ - //TODO set the bind host + if(data->mode == unconfigured){ + fprintf(stderr, "Please specify mode for instance %s before setting bind host\n", inst->name); + return 1; + } + + mmbackend_parse_hostspec(value, &host, &port); + + if(!host){ + fprintf(stderr, "Could not parse bind host specification %s for instance %s\n", value, inst->name); + return 1; + } + + return rtpmidi_bind_instance(data, host, port); } else if(!strcmp(option, "learn")){ if(data->mode != direct){ @@ -198,6 +239,8 @@ static instance* rtpmidi_instance(){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; } + data->fd = -1; + data->control_fd = -1; inst->impl = data; return inst; @@ -281,15 +324,29 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ } static int rtpmidi_start(){ - size_t n, u; + size_t n, u, fds = 0; int rv = 1; instance** inst = NULL; rtpmidi_instance_data* data = NULL; - //TODO if mdns name defined and no socket, bind default values + //if mdns name defined and no socket, bind default values + if(cfg.mdns_name && cfg.mdns_fd < 0){ + cfg.mdns_fd = mmbackend_socket("::", RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); + if(cfg.mdns_fd < 0){ + return 1; + } + } - if(cfg.mdns_fd < 0){ - fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session support disabled\n"); + //register mdns fd to core + if(cfg.mdns_fd >= 0){ + if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ + fprintf(stderr, "rtpmidi failed to register mDNS socket with core\n"); + goto bail; + } + fds++; + } + else{ + fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session discovery disabled\n"); } //fetch all defined instances @@ -310,8 +367,22 @@ static int rtpmidi_start(){ if(!data->ssrc){ data->ssrc = rand() << 16 | rand(); } + + //if not bound, bind to default + if(data->fd < 0 && rtpmidi_bind_instance(data, "::", NULL)){ + fprintf(stderr, "Failed to bind default sockets for rtpmidi instance %s\n", inst[u]->name); + goto bail; + } + + //register fds to core + if(mm_manage_fd(data->fd, BACKEND_NAME, 1, NULL) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, NULL))){ + fprintf(stderr, "rtpmidi failed to register instance socket with core\n"); + goto bail; + } + fds += (data->control_fd >= 0) ? 2 : 1; } + fprintf(stderr, "rtpmidi backend registered %" PRIsize_t " descriptors to core\n", fds); rv = 0; bail: free(inst); diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index c076fd0..6cab225 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -1,4 +1,6 @@ +#ifndef _WIN32 #include +#endif #include "midimonster.h" int init(); diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index c84c5b3..c208bf7 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -23,7 +23,7 @@ stream, which may lead to inconsistencies during playback. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| | `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration | -| `mdns-bind` | `10.1.2.1 5353` | `0.0.0.0 5353` | Bind host for the mDNS discovery server | +| `mdns-bind` | `10.1.2.1 5353` | `:: 5353` | Bind host for the mDNS discovery server | | `mdns-name` | `computer1` | none | mDNS hostname to announce, also used as AppleMIDI peer name | #### Instance configuration @@ -39,7 +39,7 @@ Common instance configuration parameters | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to | +| `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to | | `learn` | `true` | `false` | Accept new peers for data exchange at runtime | | `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times | @@ -47,12 +47,12 @@ Common instance configuration parameters | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | +| `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | | `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | | `join` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | -Note that AppleMIDI session establishment requires mDNS functionality, thus the `mdns-name` global parameter +Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter (and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. #### Channel specification -- cgit v1.2.3 From 1bde6249ab6b44764ff2d7ef79e2f22ddf9a7082 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 11 Dec 2019 23:30:46 +0100 Subject: rtpmidi shutdown procedure --- backends/rtpmidi.c | 44 ++++++++++++++++++++++++++++++++------------ backends/rtpmidi.h | 5 ++--- 2 files changed, 34 insertions(+), 15 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 38cc9c1..77819ee 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -323,10 +323,9 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ return 1; } -static int rtpmidi_start(){ - size_t n, u, fds = 0; +static int rtpmidi_start(size_t n, instance** inst){ + size_t u, fds = 0; int rv = 1; - instance** inst = NULL; rtpmidi_instance_data* data = NULL; //if mdns name defined and no socket, bind default values @@ -349,12 +348,6 @@ static int rtpmidi_start(){ fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session discovery disabled\n"); } - //fetch all defined instances - if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ - fprintf(stderr, "Failed to fetch instance list\n"); - return 1; - } - for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; //check whether instances are explicitly configured to a mode @@ -385,12 +378,39 @@ static int rtpmidi_start(){ fprintf(stderr, "rtpmidi backend registered %" PRIsize_t " descriptors to core\n", fds); rv = 0; bail: - free(inst); return rv; } -static int rtpmidi_shutdown(){ - //TODO cleanup instance data +static int rtpmidi_shutdown(size_t n, instance** inst){ + rtpmidi_instance_data* data = NULL; + size_t u; + + for(u = 0; u < n; u++){ + data = (rtpmidi_instance_data*) inst[u]->impl; + if(data->fd >= 0){ + close(data->fd); + } + + if(data->control_fd >= 0){ + close(data->control_fd); + } + + free(data->session_name); + data->session_name = NULL; + + free(data->invite_peers); + data->invite_peers = NULL; + + free(data->invite_accept); + data->invite_accept = NULL; + + free(data->peer); + data->peer = NULL; + data->peers = 0; + + free(inst[u]->impl); + inst[u]->impl = NULL; + } free(cfg.mdns_name); if(cfg.mdns_fd >= 0){ diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 6cab225..78675bc 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -10,10 +10,9 @@ static instance* rtpmidi_instance(); static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int rtpmidi_handle(size_t num, managed_fd* fds); -static int rtpmidi_start(); -static int rtpmidi_shutdown(); +static int rtpmidi_start(size_t n, instance** inst); +static int rtpmidi_shutdown(size_t n, instance** inst); -#define RTPMIDI_DEFAULT_PORTBASE "9001" #define RTPMIDI_RECV_BUF 4096 #define RTPMIDI_MDNS_PORT "5353" #define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) -- cgit v1.2.3 From 60adf2c4fe53e935e6de359ef1c01d0a91ab7480 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 11 Dec 2019 23:49:11 +0100 Subject: Implement rtpmidi peer handling --- backends/rtpmidi.c | 76 +++++++++++++++++++++++++++++++++++++++-------------- backends/rtpmidi.h | 3 ++- backends/rtpmidi.md | 1 + 3 files changed, 60 insertions(+), 20 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 77819ee..612ac6f 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -19,7 +19,7 @@ static struct /*_rtpmidi_global*/ { .detect = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend rtpmidi = { .name = BACKEND_NAME, .conf = rtpmidi_configure, @@ -124,9 +124,34 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* return 0; } +static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len){ + size_t u; + + for(u = 0; u < data->peers; u++){ + //check whether the peer is already in the list + if(sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + return 0; + } + } + + data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); + if(!data->peer){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + data->peer[data->peers].dest = sock_addr; + data->peer[data->peers].dest_len = sock_len; + + data->peers++; + return 0; +} + static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; char* host = NULL, *port = NULL; + struct sockaddr_storage sock_addr; + socklen_t sock_len = sizeof(sock_addr); if(!strcmp(option, "mode")){ if(!strcmp(value, "direct")){ @@ -174,13 +199,18 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 0; } else if(!strcmp(option, "peer")){ - if(data->mode != direct){ - fprintf(stderr, "The rtpmidi 'peer' option is only valid for direct mode instances\n"); + mmbackend_parse_hostspec(value, &host, &port); + if(!host || !port){ + fprintf(stderr, "Invalid peer %s configured on rtpmidi instance %s\n", value, inst->name); return 1; } - //TODO add peer - return 0; + if(mmbackend_parse_sockaddr(host, port, &sock_addr, &sock_len)){ + fprintf(stderr, "Failed to resolve peer %s configured on rtpmidi instance %s\n", value, inst->name); + return 1; + } + + return rtpmidi_push_peer(data, sock_addr, sock_len); } else if(!strcmp(option, "session")){ if(data->mode != apple){ @@ -313,24 +343,34 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v } static int rtpmidi_handle(size_t num, managed_fd* fds){ - //TODO handle discovery + size_t u; + int rv = 0; + + //TODO handle mDNS discovery frames if(!num){ return 0; } - //TODO - return 1; + for(u = 0; u < num; u++){ + if(!fds[u].impl){ + //TODO handle mDNS discovery input + } + else{ + //TODO handle rtp/control input + } + } + + return rv; } static int rtpmidi_start(size_t n, instance** inst){ size_t u, fds = 0; - int rv = 1; rtpmidi_instance_data* data = NULL; //if mdns name defined and no socket, bind default values if(cfg.mdns_name && cfg.mdns_fd < 0){ - cfg.mdns_fd = mmbackend_socket("::", RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); + cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); if(cfg.mdns_fd < 0){ return 1; } @@ -340,7 +380,7 @@ static int rtpmidi_start(size_t n, instance** inst){ if(cfg.mdns_fd >= 0){ if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ fprintf(stderr, "rtpmidi failed to register mDNS socket with core\n"); - goto bail; + return 1; } fds++; } @@ -353,7 +393,7 @@ static int rtpmidi_start(size_t n, instance** inst){ //check whether instances are explicitly configured to a mode if(data->mode == unconfigured){ fprintf(stderr, "rtpmidi instance %s is missing a mode configuration\n", inst[u]->name); - goto bail; + return 1; } //generate random ssrc's @@ -362,23 +402,21 @@ static int rtpmidi_start(size_t n, instance** inst){ } //if not bound, bind to default - if(data->fd < 0 && rtpmidi_bind_instance(data, "::", NULL)){ + if(data->fd < 0 && rtpmidi_bind_instance(data, RTPMIDI_DEFAULT_HOST, NULL)){ fprintf(stderr, "Failed to bind default sockets for rtpmidi instance %s\n", inst[u]->name); - goto bail; + return 1; } //register fds to core - if(mm_manage_fd(data->fd, BACKEND_NAME, 1, NULL) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, NULL))){ + if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){ fprintf(stderr, "rtpmidi failed to register instance socket with core\n"); - goto bail; + return 1; } fds += (data->control_fd >= 0) ? 2 : 1; } fprintf(stderr, "rtpmidi backend registered %" PRIsize_t " descriptors to core\n", fds); - rv = 0; -bail: - return rv; + return 0; } static int rtpmidi_shutdown(size_t n, instance** inst){ diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 78675bc..ddb7bed 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -3,7 +3,7 @@ #endif #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int rtpmidi_configure(char* option, char* value); static int rtpmidi_configure_instance(instance* instance, char* option, char* value); static instance* rtpmidi_instance(); @@ -14,6 +14,7 @@ static int rtpmidi_start(size_t n, instance** inst); static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_RECV_BUF 4096 +#define RTPMIDI_DEFAULT_HOST "::" #define RTPMIDI_MDNS_PORT "5353" #define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index c208bf7..d8e3b63 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -51,6 +51,7 @@ Common instance configuration parameters | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | | `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | | `join` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | +| `peer` | `10.1.2.3 9001` | none | Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter (and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. -- cgit v1.2.3 From d17023d77e90a8665965da50685dcf4a30f3eb37 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 13 Dec 2019 22:43:22 +0100 Subject: Basic rtpmidi output --- backends/rtpmidi.c | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++-- backends/rtpmidi.h | 7 ++-- 2 files changed, 113 insertions(+), 6 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 612ac6f..40e2cb2 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -338,13 +338,108 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ } static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ - //TODO - return 1; + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + rtpmidi_header* rtp_header = (rtpmidi_header*) frame; + rtpmidi_command_header* command_header = (rtpmidi_command_header*) (frame + sizeof(rtpmidi_header)); + size_t offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0; + uint8_t* payload = frame + offset; + rtpmidi_channel_ident ident; + + rtp_header->vpxccmpt = RTPMIDI_HEADER_MAGIC; + rtp_header->sequence = htobe16(data->sequence++); + rtp_header->timestamp = 0; //TODO calculate appropriate timestamps + rtp_header->ssrc = htobe32(data->ssrc); + + //midi command section header + //TODO enable the journal bit here + command_header->flags = 0xA0; //extended length header, first entry in list has dtime + + //midi list + for(u = 0; u < num; u++){ + ident.label = c[u]->ident; + + //encode timestamp + payload[0] = 0; + + //encode midi command + payload[1] = ident.fields.type | ident.fields.channel; + payload[2] = ident.fields.control; + payload[3] = v[u].normalised * 127.0; + + if(ident.fields.type == pitchbend){ + payload[2] = ((int)(v[u].normalised * 16384.0)) & 0x7F; + payload[3] = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; + } + //channel-wide aftertouch is only 2 bytes + else if(ident.fields.type == aftertouch){ + payload[2] = payload[3]; + payload -= 1; + offset -= 1; + } + + payload += 4; + offset += 4; + } + + //update command section length + //FIXME this might overrun, might check the number of events at some point + command_header->flags |= (((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0x0F00) >> 8); + command_header->length = ((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0xFF); + + //TODO journal section + + for(u = 0; u < data->peers; u++){ + sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); + } + + return 0; +} + +static int rtpmidi_handle_data(instance* inst){ + size_t u; + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + struct sockaddr_storage sock_addr; + socklen_t sock_len = sizeof(sock_addr); + rtpmidi_header* rtp_header = (rtpmidi_header*) frame; + ssize_t bytes_recv = recvfrom(data->fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); + + //TODO receive until EAGAIN + //FIXME might want to filter data input from sources that are not registered peers + if(rtp_header->vpxccmpt != RTPMIDI_HEADER_MAGIC){ + fprintf(stderr, "rtpmidi instance %s received frame with invalid header magic\n", inst->name); + return 0; + } + + //try to learn peers + if(data->learn_peers){ + for(u = 0; u < data->peers; u++){ + if(data->peer[u].dest_len == sock_len + && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + break; + } + } + + if(u == data->peers){ + fprintf(stderr, "rtpmidi instance %s learned new peer\n", inst->name); + return rtpmidi_push_peer(data, sock_addr, sock_len); + } + } + return 0; +} + +static int rtpmidi_handle_control(instance* inst){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + + return 0; } static int rtpmidi_handle(size_t num, managed_fd* fds){ size_t u; int rv = 0; + instance* inst = NULL; + rtpmidi_instance_data* data = NULL; //TODO handle mDNS discovery frames @@ -357,7 +452,18 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ //TODO handle mDNS discovery input } else{ - //TODO handle rtp/control input + //handle rtp/control input + inst = (instance*) fds[u].impl; + data = (rtpmidi_instance_data*) inst->impl; + if(fds[u].fd == data->fd){ + rv |= rtpmidi_handle_data(inst); + } + else if(fds[u].fd == data->control_fd){ + rv |= rtpmidi_handle_control(inst); + } + else{ + fprintf(stderr, "rtpmidi signaled descriptor not recognized\n"); + } } } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index ddb7bed..b56000a 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -13,7 +13,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds); static int rtpmidi_start(size_t n, instance** inst); static int rtpmidi_shutdown(size_t n, instance** inst); -#define RTPMIDI_RECV_BUF 4096 +#define RTPMIDI_PACKET_BUFFER 8192 #define RTPMIDI_DEFAULT_HOST "::" #define RTPMIDI_MDNS_PORT "5353" #define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) @@ -58,6 +58,7 @@ typedef struct /*_rtmidi_instance_data*/ { size_t peers; rtpmidi_peer* peer; uint32_t ssrc; + uint16_t sequence; //apple-midi config char* session_name; @@ -103,6 +104,6 @@ typedef struct /*_rtp_midi_header*/ { typedef struct /*_rtp_midi_command*/ { uint8_t flags; - uint8_t additional_length; -} rtpmidi_command; + uint8_t length; +} rtpmidi_command_header; #pragma pack(pop) -- cgit v1.2.3 From 81c6af48f9b63171d1f407f8203f742cd64c3688 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 15 Dec 2019 22:30:06 +0100 Subject: Initial AppleMIDI session support --- backends/rtpmidi.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- backends/rtpmidi.h | 14 ++++++++-- 2 files changed, 89 insertions(+), 4 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 40e2cb2..e78b1f2 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -346,7 +346,9 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v uint8_t* payload = frame + offset; rtpmidi_channel_ident ident; - rtp_header->vpxccmpt = RTPMIDI_HEADER_MAGIC; + rtp_header->vpxcc = RTPMIDI_HEADER_MAGIC; + //some receivers seem to have problems reading rfcs and interpreting the marker bit correctly + rtp_header->mpt = (data->mode == apple ? 0 : 0x80) | RTPMIDI_HEADER_TYPE; rtp_header->sequence = htobe16(data->sequence++); rtp_header->timestamp = 0; //TODO calculate appropriate timestamps rtp_header->ssrc = htobe32(data->ssrc); @@ -396,6 +398,44 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v return 0; } +static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* data, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + apple_command* command = (apple_command*) data; + size_t u; + + //find peer if already in list + for(u = 0; u < data->peers; u++){ + if(data->peer[u].dest_len == peer_len + && !memcmp(&data->peer[u].dest, peer, peer_len)){ + break; + } + } + + if(!strncmp((char*) command->command, APPLEMIDI_INVITE, 2)){ + //TODO check whether the session is in the accept list + } + else if(!strncmp((char*) command->command, APPLEMIDI_ACCEPT, 2)){ + //TODO mark peer as in-session, start timesync + } + else if(!strncmp((char*) command->command, APPLEMIDI_REJECT, 2)){ + //TODO mark peer as rejected (or retry invitation) + } + else if(!strncmp((char*) command->command, APPLEMIDI_LEAVE, 2)){ + //TODO mark peer as disconnected, retry invitation + } + else if(!strncmp((char*) command->command, APPLEMIDI_SYNC, 2)){ + //TODO respond with sync answer + } + else if(!strncmp((char*) command->command, APPLEMIDI_FEEDBACK, 2)){ + //ignore + } + else{ + fprintf(stderr, "Unknown AppleMIDI session command %02X %02X\n", command->command[0], command->command[1]); + } + + return 0; +} + static int rtpmidi_handle_data(instance* inst){ size_t u; rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; @@ -406,12 +446,27 @@ static int rtpmidi_handle_data(instance* inst){ ssize_t bytes_recv = recvfrom(data->fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); //TODO receive until EAGAIN + if(bytes_recv < 0){ + fprintf(stderr, "rtpmidi failed to receive for instance %s\n", inst->name); + return 1; + } + + if(bytes_recv < sizeof(rtpmidi_header)){ + fprintf(stderr, "Skipping short packet on rtpmidi instance %s\n", inst->name); + return 0; + } + //FIXME might want to filter data input from sources that are not registered peers - if(rtp_header->vpxccmpt != RTPMIDI_HEADER_MAGIC){ + if(data->mode == apple && rtp_header->vpxcc == 0xFF && rtp_header->mpt == 0xFF){ + return rtpmidi_handle_applemidi(inst, data->fd, frame, bytes_recv, &sock_addr, sock_len); + } + else if(rtp_header->vpxcc != RTPMIDI_HEADER_MAGIC || RTPMIDI_GET_TYPE(rtp_header->mpt) != RTPMIDI_HEADER_TYPE){ fprintf(stderr, "rtpmidi instance %s received frame with invalid header magic\n", inst->name); return 0; } + //TODO parse data + //try to learn peers if(data->learn_peers){ for(u = 0; u < data->peers; u++){ @@ -431,7 +486,27 @@ static int rtpmidi_handle_data(instance* inst){ static int rtpmidi_handle_control(instance* inst){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + struct sockaddr_storage sock_addr; + socklen_t sock_len = sizeof(sock_addr); + ssize_t bytes_recv = recvfrom(data->control_fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); + + if(bytes_recv < 0){ + fprintf(stderr, "rtpmidi failed to receive for instance %s\n", inst->name); + return 1; + } + + //the shortest applemidi packet is still larger than the rtpmidi header, so use that as bar + if(bytes_recv < sizeof(rtpmidi_header)){ + fprintf(stderr, "Skipping short packet on rtpmidi instance %s\n", inst->name); + return 0; + } + + if(data->mode == apple && frame[0] == 0xFF && frame[1] == 0xFF){ + return rtpmidi_handle_applemidi(inst, data->control_fd, frame, bytes_recv, &sock_addr, sock_len); + } + fprintf(stderr, "Unknown session protocol frame received on rtpmidi instance %s\n", inst->name); return 0; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index b56000a..d4ca044 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -16,7 +16,16 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_PACKET_BUFFER 8192 #define RTPMIDI_DEFAULT_HOST "::" #define RTPMIDI_MDNS_PORT "5353" -#define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) +#define RTPMIDI_HEADER_MAGIC 0x80 +#define RTPMIDI_HEADER_TYPE 0x61 +#define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) + +#define APPLEMIDI_INVITE "IN" +#define APPLEMIDI_ACCEPT "OK" +#define APPLEMIDI_REJECT "NO" +#define APPLEMIDI_LEAVE "BY" +#define APPLEMIDI_SYNC "CK" +#define APPLEMIDI_FEEDBACK "RS" enum /*_rtpmidi_channel_type*/ { none = 0, @@ -96,7 +105,8 @@ typedef struct /*_apple_session_feedback*/ { } apple_feedback; typedef struct /*_rtp_midi_header*/ { - uint16_t vpxccmpt; //this is really just an amalgamated constant value + uint8_t vpxcc; + uint8_t mpt; uint16_t sequence; uint32_t timestamp; uint32_t ssrc; -- cgit v1.2.3 From 86ca5c25b1c5f71c99edbd59f179c4e5f88e16e5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 15 Dec 2019 22:40:06 +0100 Subject: Fix overloading --- backends/rtpmidi.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index e78b1f2..780e517 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -398,9 +398,9 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v return 0; } -static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* data, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){ +static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; - apple_command* command = (apple_command*) data; + apple_command* command = (apple_command*) frame; size_t u; //find peer if already in list -- cgit v1.2.3 From b5d5f26835ea8840fc3aedd38780f3025d2959b3 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 16 Dec 2019 22:47:52 +0100 Subject: Move active invitations to global scope --- backends/rtpmidi.c | 85 ++++++++++++++++++++++++++++++++++++++++++----------- backends/rtpmidi.h | 9 ++++-- backends/rtpmidi.md | 6 ++-- 3 files changed, 78 insertions(+), 22 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 780e517..719f823 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -13,10 +13,15 @@ static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; uint8_t detect; + + size_t announces; + rtpmidi_announce* announce; } cfg = { .mdns_fd = -1, .mdns_name = NULL, - .detect = 0 + .detect = 0, + .announces = 0, + .announce = NULL }; MM_PLUGIN_API int init(){ @@ -137,6 +142,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); if(!data->peer){ fprintf(stderr, "Failed to allocate memory\n"); + data->peers = 0; return 1; } @@ -147,6 +153,58 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag return 0; } +static int rtpmidi_push_invite(instance* inst, char* peer){ + size_t u, p; + + //check whether the instance is already in the announce list + for(u = 0; u < cfg.announces; u++){ + if(cfg.announce[u].inst == inst){ + break; + } + } + + //add to the announce list + if(u == cfg.announces){ + cfg.announce = realloc(cfg.announce, (cfg.announces + 1) * sizeof(rtpmidi_announce)); + if(!cfg.announce){ + fprintf(stderr, "Failed to allocate memory\n"); + cfg.announces = 0; + return 1; + } + + cfg.announce[u].inst = inst; + cfg.announce[u].invites = 0; + cfg.announce[u].invite = NULL; + + cfg.announces++; + } + + //check whether the peer is already in the invite list + for(p = 0; p < cfg.announce[u].invites; p++){ + if(!strcmp(cfg.announce[u].invite[p], peer)){ + return 0; + } + } + + //extend the invite list + cfg.announce[u].invite = realloc(cfg.announce[u].invite, (cfg.announce[u].invites + 1) * sizeof(char*)); + if(!cfg.announce[u].invite){ + fprintf(stderr, "Failed to allocate memory\n"); + cfg.announce[u].invites = 0; + return 1; + } + + //append the new invitee + cfg.announce[u].invite[p] = strdup(peer); + if(!cfg.announce[u].invite[p]){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + cfg.announce[u].invites++; + return 0; +} + static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; char* host = NULL, *port = NULL; @@ -230,22 +288,17 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) fprintf(stderr, "The rtpmidi 'invite' option is only valid for apple mode instances\n"); return 1; } - free(data->invite_peers); - data->invite_peers = strdup(value); - if(!data->invite_peers){ - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - return 0; + + return rtpmidi_push_invite(inst, value); } else if(!strcmp(option, "join")){ if(data->mode != apple){ fprintf(stderr, "The rtpmidi 'join' option is only valid for apple mode instances\n"); return 1; } - free(data->invite_accept); - data->invite_accept = strdup(value); - if(!data->invite_accept){ + free(data->accept); + data->accept = strdup(value); + if(!data->accept){ fprintf(stderr, "Failed to allocate memory\n"); return 1; } @@ -350,7 +403,7 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v //some receivers seem to have problems reading rfcs and interpreting the marker bit correctly rtp_header->mpt = (data->mode == apple ? 0 : 0x80) | RTPMIDI_HEADER_TYPE; rtp_header->sequence = htobe16(data->sequence++); - rtp_header->timestamp = 0; //TODO calculate appropriate timestamps + rtp_header->timestamp = mm_timestamp() * 10; //just assume 100msec resolution because rfc4695 handwaves it rtp_header->ssrc = htobe32(data->ssrc); //midi command section header @@ -617,11 +670,8 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ free(data->session_name); data->session_name = NULL; - free(data->invite_peers); - data->invite_peers = NULL; - - free(data->invite_accept); - data->invite_accept = NULL; + free(data->accept); + data->accept = NULL; free(data->peer); data->peer = NULL; @@ -631,6 +681,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ inst[u]->impl = NULL; } + //TODO free announces free(cfg.mdns_name); if(cfg.mdns_fd >= 0){ close(cfg.mdns_fd); diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index d4ca044..2652db7 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -71,13 +71,18 @@ typedef struct /*_rtmidi_instance_data*/ { //apple-midi config char* session_name; - char* invite_peers; - char* invite_accept; + char* accept; //direct mode config uint8_t learn_peers; } rtpmidi_instance_data; +typedef struct /*rtpmidi_announced_instance*/ { + instance* inst; + size_t invites; + char** invite; +} rtpmidi_announce; + #pragma pack(push, 1) typedef struct /*_apple_session_command*/ { uint16_t res1; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index d8e3b63..d42df6f 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -34,6 +34,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 protocols | `direct` mode instance configuration parameters @@ -41,7 +42,6 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to | | `learn` | `true` | `false` | Accept new peers for data exchange at runtime | -| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times | `apple` mode instance configuration parameters @@ -49,8 +49,8 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | -| `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | -| `join` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | +| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times | +| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | | `peer` | `10.1.2.3 9001` | none | Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter -- cgit v1.2.3 From 2ba935f9a435624d57bfc57cf6262898987c4a08 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 22 Dec 2019 13:52:37 +0100 Subject: Update rtpmidi to conform to master changes --- backends/rtpmidi.c | 105 +++++++++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 52 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 719f823..e842a84 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,3 +1,5 @@ +#define BACKEND_NAME "rtpmidi" + #include #include #include @@ -7,8 +9,6 @@ #include "libmmbackend.h" #include "rtpmidi.h" -#define BACKEND_NAME "rtpmidi" - static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; @@ -38,12 +38,12 @@ MM_PLUGIN_API int init(){ }; if(sizeof(rtpmidi_channel_ident) != sizeof(uint64_t)){ - fprintf(stderr, "rtpmidi channel identification union out of bounds\n"); + LOG("Channel identification union out of bounds"); return 1; } if(mm_backend_register(rtpmidi)){ - fprintf(stderr, "Failed to register rtpmidi backend\n"); + LOG("Failed to register backend"); return 1; } @@ -55,33 +55,33 @@ static int rtpmidi_configure(char* option, char* value){ if(!strcmp(option, "mdns-name")){ if(cfg.mdns_name){ - fprintf(stderr, "Duplicate mdns-name assignment\n"); + LOG("Duplicate mdns-name assignment"); return 1; } cfg.mdns_name = strdup(value); if(!cfg.mdns_name){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return 1; } return 0; } else if(!strcmp(option, "mdns-bind")){ if(cfg.mdns_fd >= 0){ - fprintf(stderr, "Only one mDNS discovery bind is supported\n"); + LOG( "Only one mDNS discovery bind is supported"); return 1; } - mmbackend_parse_hostspec(value, &host, &port); + mmbackend_parse_hostspec(value, &host, &port, NULL); if(!host){ - fprintf(stderr, "Not a valid mDNS bind address: %s\n", value); + LOGPF("Not a valid mDNS bind address: %s", value); return 1; } cfg.mdns_fd = mmbackend_socket(host, (port ? port : RTPMIDI_MDNS_PORT), SOCK_DGRAM, 1, 1); if(cfg.mdns_fd < 0){ - fprintf(stderr, "Failed to bind mDNS interface: %s\n", value); + LOGPF("Failed to bind mDNS interface: %s", value); return 1; } return 0; @@ -94,7 +94,7 @@ static int rtpmidi_configure(char* option, char* value){ return 0; } - fprintf(stderr, "Unknown rtpmidi backend option %s\n", option); + LOGPF("Unknown backend configuration option %s", option); return 1; } @@ -114,14 +114,14 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* //bind control port if(data->mode == apple){ if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ - fprintf(stderr, "Failed to fetch data port information: %s\n", strerror(errno)); + LOGPF("Failed to fetch data port information: %s", strerror(errno)); return 1; } snprintf(control_port, sizeof(control_port), "%d", be16toh(((struct sockaddr_in*)&sock_addr)->sin_port) - 1); data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0); if(data->control_fd < 0){ - fprintf(stderr, "Failed to bind control port %s\n", control_port); + LOGPF("Failed to bind control port %s", control_port); return 1; } } @@ -141,7 +141,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); if(!data->peer){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); data->peers = 0; return 1; } @@ -167,7 +167,7 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ if(u == cfg.announces){ cfg.announce = realloc(cfg.announce, (cfg.announces + 1) * sizeof(rtpmidi_announce)); if(!cfg.announce){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); cfg.announces = 0; return 1; } @@ -189,7 +189,7 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ //extend the invite list cfg.announce[u].invite = realloc(cfg.announce[u].invite, (cfg.announce[u].invites + 1) * sizeof(char*)); if(!cfg.announce[u].invite){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); cfg.announce[u].invites = 0; return 1; } @@ -197,7 +197,7 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ //append the new invitee cfg.announce[u].invite[p] = strdup(peer); if(!cfg.announce[u].invite[p]){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return 1; } @@ -220,26 +220,26 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) data->mode = apple; return 0; } - fprintf(stderr, "Unknown rtpmidi instance mode %s for instance %s\n", value, inst->name); + LOGPF("Unknown instance mode %s for instance %s", value, inst->name); return 1; } else if(!strcmp(option, "ssrc")){ data->ssrc = strtoul(value, NULL, 0); if(!data->ssrc){ - fprintf(stderr, "Random SSRC will be generated for rtpmidi instance %s\n", inst->name); + LOGPF("Random SSRC will be generated for instance %s", inst->name); } return 0; } else if(!strcmp(option, "bind")){ if(data->mode == unconfigured){ - fprintf(stderr, "Please specify mode for instance %s before setting bind host\n", inst->name); + LOGPF("Please specify mode for instance %s before setting bind host", inst->name); return 1; } - mmbackend_parse_hostspec(value, &host, &port); + mmbackend_parse_hostspec(value, &host, &port, NULL); if(!host){ - fprintf(stderr, "Could not parse bind host specification %s for instance %s\n", value, inst->name); + LOGPF("Could not parse bind host specification %s for instance %s", value, inst->name); return 1; } @@ -247,7 +247,7 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) } else if(!strcmp(option, "learn")){ if(data->mode != direct){ - fprintf(stderr, "The rtpmidi 'learn' option is only valid for direct mode instances\n"); + LOG("'learn' option is only valid for direct mode instances"); return 1; } data->learn_peers = 0; @@ -257,14 +257,14 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 0; } else if(!strcmp(option, "peer")){ - mmbackend_parse_hostspec(value, &host, &port); + mmbackend_parse_hostspec(value, &host, &port, NULL); if(!host || !port){ - fprintf(stderr, "Invalid peer %s configured on rtpmidi instance %s\n", value, inst->name); + LOGPF("Invalid peer %s configured on instance %s", value, inst->name); return 1; } if(mmbackend_parse_sockaddr(host, port, &sock_addr, &sock_len)){ - fprintf(stderr, "Failed to resolve peer %s configured on rtpmidi instance %s\n", value, inst->name); + LOGPF("Failed to resolve peer %s on instance %s", value, inst->name); return 1; } @@ -272,20 +272,20 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) } else if(!strcmp(option, "session")){ if(data->mode != apple){ - fprintf(stderr, "The rtpmidi 'session' option is only valid for apple mode instances\n"); + LOG("'session' option is only valid for apple mode instances"); return 1; } free(data->session_name); data->session_name = strdup(value); if(!data->session_name){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return 1; } return 0; } else if(!strcmp(option, "invite")){ if(data->mode != apple){ - fprintf(stderr, "The rtpmidi 'invite' option is only valid for apple mode instances\n"); + LOG("'invite' option is only valid for apple mode instances"); return 1; } @@ -293,19 +293,19 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) } else if(!strcmp(option, "join")){ if(data->mode != apple){ - fprintf(stderr, "The rtpmidi 'join' option is only valid for apple mode instances\n"); + LOG("'join' option is only valid for apple mode instances"); return 1; } free(data->accept); data->accept = strdup(value); if(!data->accept){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return 1; } return 0; } - fprintf(stderr, "Unknown rtpmidi instance option %s\n", option); + LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); return 1; } @@ -319,7 +319,7 @@ static instance* rtpmidi_instance(){ data = calloc(1, sizeof(rtpmidi_instance_data)); if(!data){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return NULL; } data->fd = -1; @@ -342,18 +342,18 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ } } else{ - fprintf(stderr, "Invalid rtpmidi channel specification %s\n", spec); + LOGPF("Invalid channel specification %s", spec); return NULL; } ident.fields.channel = strtoul(next_token, &next_token, 10); if(ident.fields.channel > 15){ - fprintf(stderr, "rtpmidi channel out of range in channel spec %s\n", spec); + LOGPF("Channel out of range in channel spec %s", spec); return NULL; } if(*next_token != '.'){ - fprintf(stderr, "rtpmidi channel specification %s does not conform to channel.\n", spec); + LOGPF("Channel specification %s does not conform to channel.", spec); return NULL; } @@ -378,7 +378,7 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ ident.fields.type = aftertouch; } else{ - fprintf(stderr, "Unknown rtpmidi channel control type in spec %s\n", spec); + LOGPF("Unknown control type in spec %s", spec); return NULL; } @@ -483,7 +483,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size //ignore } else{ - fprintf(stderr, "Unknown AppleMIDI session command %02X %02X\n", command->command[0], command->command[1]); + LOGPF("Unknown AppleMIDI session command %02X %02X", command->command[0], command->command[1]); } return 0; @@ -500,12 +500,12 @@ static int rtpmidi_handle_data(instance* inst){ //TODO receive until EAGAIN if(bytes_recv < 0){ - fprintf(stderr, "rtpmidi failed to receive for instance %s\n", inst->name); + LOGPF("Failed to receive for instance %s", inst->name); return 1; } if(bytes_recv < sizeof(rtpmidi_header)){ - fprintf(stderr, "Skipping short packet on rtpmidi instance %s\n", inst->name); + LOGPF("Skipping short packet on instance %s", inst->name); return 0; } @@ -514,7 +514,7 @@ static int rtpmidi_handle_data(instance* inst){ return rtpmidi_handle_applemidi(inst, data->fd, frame, bytes_recv, &sock_addr, sock_len); } else if(rtp_header->vpxcc != RTPMIDI_HEADER_MAGIC || RTPMIDI_GET_TYPE(rtp_header->mpt) != RTPMIDI_HEADER_TYPE){ - fprintf(stderr, "rtpmidi instance %s received frame with invalid header magic\n", inst->name); + LOGPF("Frame with invalid header magic on %s", inst->name); return 0; } @@ -530,7 +530,7 @@ static int rtpmidi_handle_data(instance* inst){ } if(u == data->peers){ - fprintf(stderr, "rtpmidi instance %s learned new peer\n", inst->name); + LOGPF("Learned new peer on %s", inst->name); return rtpmidi_push_peer(data, sock_addr, sock_len); } } @@ -545,13 +545,13 @@ static int rtpmidi_handle_control(instance* inst){ ssize_t bytes_recv = recvfrom(data->control_fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); if(bytes_recv < 0){ - fprintf(stderr, "rtpmidi failed to receive for instance %s\n", inst->name); + LOGPF("Failed to receive on control socket for instance %s", inst->name); return 1; } //the shortest applemidi packet is still larger than the rtpmidi header, so use that as bar if(bytes_recv < sizeof(rtpmidi_header)){ - fprintf(stderr, "Skipping short packet on rtpmidi instance %s\n", inst->name); + LOGPF("Skipping short packet on control socket of instance %s", inst->name); return 0; } @@ -559,7 +559,7 @@ static int rtpmidi_handle_control(instance* inst){ return rtpmidi_handle_applemidi(inst, data->control_fd, frame, bytes_recv, &sock_addr, sock_len); } - fprintf(stderr, "Unknown session protocol frame received on rtpmidi instance %s\n", inst->name); + LOGPF("Unknown session protocol frame received on instance %s", inst->name); return 0; } @@ -590,7 +590,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ rv |= rtpmidi_handle_control(inst); } else{ - fprintf(stderr, "rtpmidi signaled descriptor not recognized\n"); + LOG("Signaled for unknown descriptor"); } } } @@ -613,20 +613,20 @@ static int rtpmidi_start(size_t n, instance** inst){ //register mdns fd to core if(cfg.mdns_fd >= 0){ if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ - fprintf(stderr, "rtpmidi failed to register mDNS socket with core\n"); + LOG("Failed to register mDNS socket with core"); return 1; } fds++; } else{ - fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session discovery disabled\n"); + LOG("No mDNS discovery interface bound, AppleMIDI session discovery disabled"); } for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; //check whether instances are explicitly configured to a mode if(data->mode == unconfigured){ - fprintf(stderr, "rtpmidi instance %s is missing a mode configuration\n", inst[u]->name); + LOGPF("Instance %s is missing a mode configuration", inst[u]->name); return 1; } @@ -637,19 +637,19 @@ static int rtpmidi_start(size_t n, instance** inst){ //if not bound, bind to default if(data->fd < 0 && rtpmidi_bind_instance(data, RTPMIDI_DEFAULT_HOST, NULL)){ - fprintf(stderr, "Failed to bind default sockets for rtpmidi instance %s\n", inst[u]->name); + LOGPF("Failed to bind default sockets for instance %s", inst[u]->name); return 1; } //register fds to core if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){ - fprintf(stderr, "rtpmidi failed to register instance socket with core\n"); + LOGPF("Failed to register descriptor for instance %s with core", inst[u]->name); return 1; } fds += (data->control_fd >= 0) ? 2 : 1; } - fprintf(stderr, "rtpmidi backend registered %" PRIsize_t " descriptors to core\n", fds); + LOGPF("Registered %" PRIsize_t " descriptors to core", fds); return 0; } @@ -687,5 +687,6 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ close(cfg.mdns_fd); } + LOG("Backend shut down"); return 0; } -- cgit v1.2.3 From 474077a1ad13f945f2fd3dc6199e67eeeb86f517 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 24 Dec 2019 00:48:00 +0100 Subject: Implement AppleMIDI responder mode --- backends/rtpmidi.c | 180 +++++++++++++++++++++++++++++++++++++++++++--------- backends/rtpmidi.h | 32 ++++++---- backends/rtpmidi.md | 6 +- 3 files changed, 172 insertions(+), 46 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index e842a84..fc7af26 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "rtpmidi" +#define DEBUG #include #include @@ -9,6 +10,9 @@ #include "libmmbackend.h" #include "rtpmidi.h" +//TODO learn peer ssrcs +//TODO participants need to initiate clock sync at some point + static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; @@ -130,26 +134,35 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* } static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len){ - size_t u; + size_t u, p = data->peers; for(u = 0; u < data->peers; u++){ //check whether the peer is already in the list - if(sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + if(!data->peer[u].inactive + && sock_len == data->peer[u].dest_len + && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ return 0; } - } - data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); - if(!data->peer){ - LOG("Failed to allocate memory"); - data->peers = 0; - return 1; + if(data->peer[u].inactive){ + p = u; + } } - data->peer[data->peers].dest = sock_addr; - data->peer[data->peers].dest_len = sock_len; + if(p == data->peers){ + data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); + if(!data->peer){ + LOG("Failed to allocate memory"); + data->peers = 0; + return 1; + } + data->peers++; + DBGPF("Extending peer registry to %" PRIsize_t " entries", data->peers); + } - data->peers++; + data->peer[p].inactive = 0; + data->peer[p].dest = sock_addr; + data->peer[p].dest_len = sock_len; return 0; } @@ -445,7 +458,9 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v //TODO journal section for(u = 0; u < data->peers; u++){ - sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); + if(!data->peer[u].inactive){ + sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); + } } return 0; @@ -453,37 +468,141 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t response[RTPMIDI_PACKET_BUFFER] = ""; apple_command* command = (apple_command*) frame; - size_t u; + char* session_name = (char*) frame + sizeof(apple_command); + size_t u, n; + + command->command = be16toh(command->command); + + //check command version (except for clock sync and receiver feedback) + if(command->command != apple_sync && command->command != apple_feedback + && be32toh(command->version) != 2){ + LOGPF("Invalid AppleMIDI command version %" PRIu32 " on instance %s", be32toh(command->version), inst->name); + return 0; + } //find peer if already in list for(u = 0; u < data->peers; u++){ - if(data->peer[u].dest_len == peer_len + if(!data->peer[u].inactive + && data->peer[u].dest_len == peer_len && !memcmp(&data->peer[u].dest, peer, peer_len)){ break; } } - if(!strncmp((char*) command->command, APPLEMIDI_INVITE, 2)){ - //TODO check whether the session is in the accept list - } - else if(!strncmp((char*) command->command, APPLEMIDI_ACCEPT, 2)){ - //TODO mark peer as in-session, start timesync + if(command->command == apple_invite){ + //check session name + for(n = sizeof(apple_command); n < bytes; n++){ + if(!frame[n]){ + break; + } + + if(!isprint(frame[n])){ + session_name = NULL; + break; + } + } + + //unterminated string + if(n == bytes){ + session_name = NULL; + } + + //FIXME if already in session, reject the invitation + if(data->accept && + (!strcmp(data->accept, "*") || (session_name && !strcmp(session_name, data->accept)))){ + //accept the invitation + LOGPF("Instance %s accepting invitation to session %s%s", inst->name, session_name ? session_name : "UNNAMED", (fd == data->control_fd) ? " (control)":""); + //send accept message + apple_command* accept = (apple_command*) response; + accept->res1 = 0xFFFF; + accept->command = htobe16(apple_accept); + accept->version = htobe32(2); + accept->token = command->token; + accept->ssrc = htobe32(data->ssrc); + //add local name to response + //FIXME might want to use the session name in case it is set + if(cfg.mdns_name){ + memcpy(response + sizeof(apple_command), cfg.mdns_name, strlen(cfg.mdns_name) + 1); + } + else{ + memcpy(response + sizeof(apple_command), RTPMIDI_DEFAULT_NAME, strlen(RTPMIDI_DEFAULT_NAME) + 1); + } + sendto(fd, response, sizeof(apple_command) + strlen(cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); + + //push peer + if(fd != data->control_fd){ + return rtpmidi_push_peer(data, *peer, peer_len); + } + return 0; + } + else{ + //send reject message + LOGPF("Instance %s rejecting invitation to session %s", inst->name, session_name ? session_name : "UNNAMED"); + apple_command reject = { + .res1 = 0xFFFF, + .command = htobe16(apple_reject), + .version = htobe32(2), + .token = command->token, + .ssrc = htobe32(data->ssrc) + }; + sendto(fd, (uint8_t*) &reject, sizeof(apple_command), 0, (struct sockaddr*) peer, peer_len); + } + } + else if(command->command == apple_accept){ + if(fd != data->control_fd){ + return rtpmidi_push_peer(data, *peer, peer_len); + //FIXME store ssrc, start timesync + } + else{ + //TODO send invite on data fd + + } } - else if(!strncmp((char*) command->command, APPLEMIDI_REJECT, 2)){ - //TODO mark peer as rejected (or retry invitation) + else if(command->command == apple_reject){ + //just ignore this for now and retry the invitation } - else if(!strncmp((char*) command->command, APPLEMIDI_LEAVE, 2)){ - //TODO mark peer as disconnected, retry invitation + else if(command->command == apple_leave){ + //remove peer from list + if(u != data->peers){ + data->peer[u].inactive = 1; + } } - else if(!strncmp((char*) command->command, APPLEMIDI_SYNC, 2)){ - //TODO respond with sync answer + else if(command->command == apple_sync){ + //respond with sync answer + memcpy(response, frame, bytes); + apple_sync_frame* sync = (apple_sync_frame*) response; + DBGPF("Incoming sync on instance %s (%d)", inst->name, sync->count); + sync->command = htobe16(apple_sync); + sync->ssrc = htobe32(data->ssrc); + switch(sync->count){ + case 0: + //this happens if we're a participant + sync->count++; + sync->timestamp[1] = htobe64(mm_timestamp() * 10); + break; + case 1: + //this happens if we're an initiator + sync->count++; + sync->timestamp[2] = htobe64(mm_timestamp() * 10); + break; + default: + //ignore this one + return 0; + } + + sendto(fd, response, sizeof(apple_sync_frame), 0, (struct sockaddr*) peer, peer_len); + return 0; } - else if(!strncmp((char*) command->command, APPLEMIDI_FEEDBACK, 2)){ - //ignore + else if(command->command == apple_feedback){ + if(u != data->peers){ + //TODO store this somewhere to properly update the recovery journal + LOGPF("Feedback on instance %s", inst->name); + } } else{ - LOGPF("Unknown AppleMIDI session command %02X %02X", command->command[0], command->command[1]); + LOGPF("Unknown AppleMIDI session command %04X", command->command); } return 0; @@ -523,7 +642,8 @@ static int rtpmidi_handle_data(instance* inst){ //try to learn peers if(data->learn_peers){ for(u = 0; u < data->peers; u++){ - if(data->peer[u].dest_len == sock_len + if(!data->peer[u].inactive + && data->peer[u].dest_len == sock_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ break; } @@ -632,7 +752,7 @@ static int rtpmidi_start(size_t n, instance** inst){ //generate random ssrc's if(!data->ssrc){ - data->ssrc = rand() << 16 | rand(); + data->ssrc = ((uint32_t) rand()) << 16 | rand(); } //if not bound, bind to default diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 2652db7..a9effd8 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -19,13 +19,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_HEADER_MAGIC 0x80 #define RTPMIDI_HEADER_TYPE 0x61 #define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) - -#define APPLEMIDI_INVITE "IN" -#define APPLEMIDI_ACCEPT "OK" -#define APPLEMIDI_REJECT "NO" -#define APPLEMIDI_LEAVE "BY" -#define APPLEMIDI_SYNC "CK" -#define APPLEMIDI_FEEDBACK "RS" +#define RTPMIDI_DEFAULT_NAME "MIDIMonster" enum /*_rtpmidi_channel_type*/ { none = 0, @@ -55,7 +49,8 @@ typedef union { typedef struct /*_rtpmidi_peer*/ { struct sockaddr_storage dest; socklen_t dest_len; - uint32_t ssrc; + //uint32_t ssrc; + uint8_t inactive; } rtpmidi_peer; typedef struct /*_rtmidi_instance_data*/ { @@ -70,8 +65,8 @@ typedef struct /*_rtmidi_instance_data*/ { uint16_t sequence; //apple-midi config - char* session_name; - char* accept; + char* session_name; /* initiator only */ + char* accept; /* participant only */ //direct mode config uint8_t learn_peers; @@ -83,10 +78,19 @@ typedef struct /*rtpmidi_announced_instance*/ { char** invite; } rtpmidi_announce; +enum applemidi_command { + apple_invite = 0x494E, //IN + apple_accept = 0x4F4B, //OK + apple_reject = 0x4E4F, //NO + apple_leave = 0x4259, //BY + apple_sync = 0x434B, //CK + apple_feedback = 0x5253 //RS +}; + #pragma pack(push, 1) typedef struct /*_apple_session_command*/ { uint16_t res1; - uint8_t command[2]; + uint16_t command; uint32_t version; uint32_t token; uint32_t ssrc; @@ -95,19 +99,19 @@ typedef struct /*_apple_session_command*/ { typedef struct /*_apple_session_sync*/ { uint16_t res1; - uint8_t command[2]; + uint16_t command; uint32_t ssrc; uint8_t count; uint8_t res2[3]; uint64_t timestamp[3]; -} apple_sync; +} apple_sync_frame; typedef struct /*_apple_session_feedback*/ { uint16_t res1; uint8_t command[2]; uint32_t ssrc; uint32_t sequence; -} apple_feedback; +} apple_journal_feedback; typedef struct /*_rtp_midi_header*/ { uint8_t vpxcc; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index d42df6f..93811c6 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -13,7 +13,9 @@ selectable per-instance, with some methods requiring additional global configura * Direct connection with peer learning: The instance will send and receive data from peers configured in the instance configuration as well as previously unknown peers that voluntarily send data to the instance. -* AppleMIDI session management: +* AppleMIDI session management: The instance will be able to communicate (either as participant + or initiator) in an AppleMIDI session, which can optionally be announced via mDNS (better + known as "Bonjour" to Apple users). Note that instances that receive data from multiple peers will combine all inputs into one stream, which may lead to inconsistencies during playback. @@ -50,7 +52,7 @@ Common instance configuration parameters | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | | `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times | -| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | +| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). Setting this option makes the instance a session participant | | `peer` | `10.1.2.3 9001` | none | Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter -- cgit v1.2.3 From 0a7858e17d8c60c5c768d310b1c7a511e91565ab Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 26 Dec 2019 09:44:17 +0100 Subject: Invite peer on data port --- backends/rtpmidi.c | 38 ++++++++++++++++++++++++++++---------- backends/rtpmidi.md | 2 +- 2 files changed, 29 insertions(+), 11 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index fc7af26..4966a79 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -12,6 +12,7 @@ //TODO learn peer ssrcs //TODO participants need to initiate clock sync at some point +//TODO applemode peers still need session negotiation (peer option should only bypass discovery) static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -523,12 +524,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size accept->ssrc = htobe32(data->ssrc); //add local name to response //FIXME might want to use the session name in case it is set - if(cfg.mdns_name){ - memcpy(response + sizeof(apple_command), cfg.mdns_name, strlen(cfg.mdns_name) + 1); - } - else{ - memcpy(response + sizeof(apple_command), RTPMIDI_DEFAULT_NAME, strlen(RTPMIDI_DEFAULT_NAME) + 1); - } + memcpy(response + sizeof(apple_command), cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME, strlen((cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME)) + 1); sendto(fd, response, sizeof(apple_command) + strlen(cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); //push peer @@ -552,12 +548,24 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size } else if(command->command == apple_accept){ if(fd != data->control_fd){ + LOGPF("Instance %s negotiated new peer\n", inst->name); return rtpmidi_push_peer(data, *peer, peer_len); //FIXME store ssrc, start timesync } else{ - //TODO send invite on data fd - + //send invite on data fd + LOGPF("Instance %s peer accepted on control port, inviting data port\n", inst->name); + //FIXME limit max length of session name + apple_command* invite = (apple_command*) response; + invite->res1 = 0xFFFF; + invite->command = htobe16(apple_invite); + invite->version = htobe32(2); + invite->token = command->token; + invite->ssrc = htobe32(data->ssrc); + memcpy(response + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); + //calculate data port + ((struct sockaddr_in*) peer)->sin_port++; + sendto(data->fd, response, sizeof(apple_command) + strlen(data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); } } else if(command->command == apple_reject){ @@ -775,7 +783,7 @@ static int rtpmidi_start(size_t n, instance** inst){ static int rtpmidi_shutdown(size_t n, instance** inst){ rtpmidi_instance_data* data = NULL; - size_t u; + size_t u, p; for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; @@ -801,8 +809,18 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ inst[u]->impl = NULL; } - //TODO free announces + for(u = 0; u < cfg.announces; u++){ + for(p = 0; p < cfg.announce[u].invites; p++){ + free(cfg.announce[u].invite[p]); + } + free(cfg.announce[u].invite); + } + free(cfg.announce); + cfg.announce = NULL; + cfg.announces = 0; + free(cfg.mdns_name); + cfg.mdns_name = NULL; if(cfg.mdns_fd >= 0){ close(cfg.mdns_fd); } diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 93811c6..e857a5a 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -36,7 +36,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 protocols | +| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times. Bypasses session discovery (but still performs session negotiation) | `direct` mode instance configuration parameters -- cgit v1.2.3 From 4275aa8d0209768e45b43ef7e3aefcb808001926 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 27 Dec 2019 17:46:37 +0100 Subject: Implement MIDI command parsing --- backends/rtpmidi.c | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 4966a79..611d99b 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -134,6 +134,22 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* return 0; } +static char* rtpmidi_type_name(uint8_t type){ + switch(type){ + case note: + return "note"; + case cc: + return "cc"; + case pressure: + return "pressure"; + case aftertouch: + return "aftertouch"; + case pitchbend: + return "pitch"; + } + return "unknown"; +} + static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len){ size_t u, p = data->peers; @@ -616,14 +632,145 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size return 0; } +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; + uint8_t midi_status = 0; + rtpmidi_channel_ident ident; + channel_value val; + channel* chan = NULL; + + if(!bytes){ + LOGPF("No command section in data on instance %s", inst->name); + return 1; + } + + //calculate midi command section length + length = frame[0] & 0x0F; + if(frame[0] & 0x80){ + //extended header + if(bytes < 2){ + LOGPF("Short command section (%" PRIsize_t " bytes) on %s, missing extended header", bytes, inst->name); + return 1; + } + length <<= 8; + length |= frame[1]; + offset = 2; + } + + command_bytes = offset + length; + DBGPF("%u/%" PRIsize_t " bytes of command section on %s, %s header, %s initial dtime", + length, bytes, inst->name, + (frame[0] & 0x80) ? "extended" : "normal", + (frame[0] & 0x20) ? "has" : "no"); + + if(command_bytes > bytes){ + LOGPF("Short command section on %s, indicated %" PRIsize_t ", had %" PRIsize_t, inst->name, command_bytes, bytes); + return 1; + } + + if(frame[0] & 0x20){ + decode_time = 1; + } + + do{ + //decode delta-time + if(decode_time){ + for(; offset < command_bytes && frame[offset] & 0x80; offset++){ + } + offset++; + } + + //section 3 of rfc6295 states that the first dtime as well as the last command may be omitted + //this may make sense on a low-speed serial line, but on a network... come on. + if(offset >= command_bytes){ + break; + } + + //check for a status byte + //TODO filter sysex + if(frame[offset] & 0x80){ + midi_status = frame[offset]; + offset++; + } + + //having variable encoding in each and every component is super annoying to check for... + if(offset >= command_bytes){ + break; + } + + ident.label = 0; + ident.fields.type = midi_status & 0xF0; + ident.fields.channel = midi_status & 0x0F; + + //single byte command + if(ident.fields.type == aftertouch){ + ident.fields.control = 0; + val.normalised = (double) frame[offset] / 127.0; + offset++; + } + //two-byte command + else{ + offset++; + if(offset >= command_bytes){ + break; + } + + if(ident.fields.type == pitchbend){ + ident.fields.control = 0; + val.normalised = (double)((frame[offset] << 7) | frame[offset - 1]) / 16384.0; + } + else{ + ident.fields.control = frame[offset - 1]; + val.normalised = (double) frame[offset] / 127.0; + } + + //fix-up note off events + if(ident.fields.type == 0x80){ + ident.fields.type = note; + val.normalised = 0; + } + + offset++; + } + + DBGPF("Decoded command type %02X channel %d control %d value %f", + ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised); + + if(cfg.detect){ + if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){ + LOGPF("Incoming data on channel %s.ch%d.%s, value %f", + inst->name, ident.fields.channel, + rtpmidi_type_name(ident.fields.type), val.normalised); + } + else{ + LOGPF("Incoming data on channel %s.ch%d.%s%d, value %f", + inst->name, ident.fields.channel, + rtpmidi_type_name(ident.fields.type), + ident.fields.control, val.normalised); + } + } + + //find channel + chan = mm_channel(inst, ident.label, 0); + if(chan){ + mm_channel_event(chan, val); + } + + decode_time = 1; + } while(offset < command_bytes); + + return 0; +} + static int rtpmidi_handle_data(instance* inst){ - size_t u; rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; struct sockaddr_storage sock_addr; socklen_t sock_len = sizeof(sock_addr); rtpmidi_header* rtp_header = (rtpmidi_header*) frame; ssize_t bytes_recv = recvfrom(data->fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); + size_t u; //TODO receive until EAGAIN if(bytes_recv < 0){ @@ -645,7 +792,11 @@ static int rtpmidi_handle_data(instance* inst){ return 0; } - //TODO parse data + //parse data + if(rtpmidi_parse(inst, frame + sizeof(rtpmidi_header), bytes_recv - sizeof(rtpmidi_header))){ + //returning errors here fails the core loop, so just return 0 to have some logging + return 0; + } //try to learn peers if(data->learn_peers){ -- cgit v1.2.3 From 9cb41bd9e07a80885fafe30c9d230615f8b5cc67 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 28 Dec 2019 14:48:22 +0100 Subject: Extend peer handling --- backends/rtpmidi.c | 111 +++++++++++++++++++++++++++++++++++++---------------- backends/rtpmidi.h | 5 ++- 2 files changed, 82 insertions(+), 34 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 611d99b..0a83fee 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -13,11 +13,15 @@ //TODO learn peer ssrcs //TODO participants need to initiate clock sync at some point //TODO applemode peers still need session negotiation (peer option should only bypass discovery) +//TODO default session join? +//TODO default mode? +//TODO internal loop mode static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; uint8_t detect; + uint64_t last_service; size_t announces; rtpmidi_announce* announce; @@ -25,6 +29,8 @@ static struct /*_rtpmidi_global*/ { .mdns_fd = -1, .mdns_name = NULL, .detect = 0, + .last_service = 0, + .announces = 0, .announce = NULL }; @@ -150,18 +156,18 @@ static char* rtpmidi_type_name(uint8_t type){ return "unknown"; } -static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len){ +static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected){ size_t u, p = data->peers; for(u = 0; u < data->peers; u++){ //check whether the peer is already in the list - if(!data->peer[u].inactive + if(data->peer[u].active && sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ return 0; } - if(data->peer[u].inactive){ + if(!data->peer[u].active){ p = u; } } @@ -177,7 +183,9 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag DBGPF("Extending peer registry to %" PRIsize_t " entries", data->peers); } - data->peer[p].inactive = 0; + data->peer[p].active = 1; + data->peer[p].learned = learned; + data->peer[p].connected = connected; data->peer[p].dest = sock_addr; data->peer[p].dest_len = sock_len; return 0; @@ -287,6 +295,11 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 0; } else if(!strcmp(option, "peer")){ + if(data->mode == unconfigured){ + LOGPF("Please specify mode for instance %s before configuring peers", inst->name); + return 1; + } + mmbackend_parse_hostspec(value, &host, &port, NULL); if(!host || !port){ LOGPF("Invalid peer %s configured on instance %s", value, inst->name); @@ -298,7 +311,12 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 1; } - return rtpmidi_push_peer(data, sock_addr, sock_len); + //apple peers are specified using the control port, but we want to store the data port as peer + if(data->mode == apple){ + ((struct sockaddr_in*) &sock_addr)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &sock_addr)->sin_port) + 1); + } + + return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0); } else if(!strcmp(option, "session")){ if(data->mode != apple){ @@ -475,7 +493,7 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v //TODO journal section for(u = 0; u < data->peers; u++){ - if(!data->peer[u].inactive){ + 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); } } @@ -488,7 +506,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size uint8_t response[RTPMIDI_PACKET_BUFFER] = ""; apple_command* command = (apple_command*) frame; char* session_name = (char*) frame + sizeof(apple_command); - size_t u, n; + size_t n, u; command->command = be16toh(command->command); @@ -499,15 +517,6 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size return 0; } - //find peer if already in list - for(u = 0; u < data->peers; u++){ - if(!data->peer[u].inactive - && data->peer[u].dest_len == peer_len - && !memcmp(&data->peer[u].dest, peer, peer_len)){ - break; - } - } - if(command->command == apple_invite){ //check session name for(n = sizeof(apple_command); n < bytes; n++){ @@ -545,7 +554,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size //push peer if(fd != data->control_fd){ - return rtpmidi_push_peer(data, *peer, peer_len); + return rtpmidi_push_peer(data, *peer, peer_len, 1, 1); } return 0; } @@ -561,11 +570,12 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size }; sendto(fd, (uint8_t*) &reject, sizeof(apple_command), 0, (struct sockaddr*) peer, peer_len); } + return 0; } else if(command->command == apple_accept){ if(fd != data->control_fd){ LOGPF("Instance %s negotiated new peer\n", inst->name); - return rtpmidi_push_peer(data, *peer, peer_len); + return rtpmidi_push_peer(data, *peer, peer_len, 1, 1); //FIXME store ssrc, start timesync } else{ @@ -580,18 +590,31 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size invite->ssrc = htobe32(data->ssrc); memcpy(response + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); //calculate data port - ((struct sockaddr_in*) peer)->sin_port++; + ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); sendto(data->fd, response, sizeof(apple_command) + strlen(data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); } + return 0; } else if(command->command == apple_reject){ //just ignore this for now and retry the invitation } else if(command->command == apple_leave){ - //remove peer from list - if(u != data->peers){ - data->peer[u].inactive = 1; + //remove peer from list - this comes in on the control port, but we need to remove the data port... + ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); + for(u = 0; u < data->peers; u++){ + if(data->peer[u].dest_len == peer_len + && !memcmp(&data->peer[u].dest, peer, peer_len)){ + LOGPF("Instance %s removed peer", inst->name); + //learned peers are marked inactive, configured peers are marked unconnected + if(data->peer[u].learned){ + data->peer[u].active = 0; + } + else{ + data->peer[u].connected = 0; + } + } } + return 0; } else if(command->command == apple_sync){ //respond with sync answer @@ -620,10 +643,9 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size return 0; } else if(command->command == apple_feedback){ - if(u != data->peers){ - //TODO store this somewhere to properly update the recovery journal - LOGPF("Feedback on instance %s", inst->name); - } + //TODO store this somewhere to properly update the recovery journal + LOGPF("Feedback on instance %s", inst->name); + return 0; } else{ LOGPF("Unknown AppleMIDI session command %04X", command->command); @@ -674,7 +696,7 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ } do{ - //decode delta-time + //decode (and ignore) delta-time if(decode_time){ for(; offset < command_bytes && frame[offset] & 0x80; offset++){ } @@ -751,7 +773,7 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ } } - //find channel + //push event chan = mm_channel(inst, ident.label, 0); if(chan){ mm_channel_event(chan, val); @@ -801,7 +823,7 @@ static int rtpmidi_handle_data(instance* inst){ //try to learn peers if(data->learn_peers){ for(u = 0; u < data->peers; u++){ - if(!data->peer[u].inactive + if(data->peer[u].active && data->peer[u].dest_len == sock_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ break; @@ -810,7 +832,7 @@ static int rtpmidi_handle_data(instance* inst){ if(u == data->peers){ LOGPF("Learned new peer on %s", inst->name); - return rtpmidi_push_peer(data, sock_addr, sock_len); + return rtpmidi_push_peer(data, sock_addr, sock_len, 1, 1); } } return 0; @@ -842,13 +864,28 @@ static int rtpmidi_handle_control(instance* inst){ return 0; } +static int rtpmidi_service(){ + + if(cfg.mdns_fd >= 0){ + //TODO send applemidi discovery packets + } + //TODO send sync packets for all connected applemidi peers + //TODO try to invite pre-defined unconnected applemidi peers + return 0; +} + static int rtpmidi_handle(size_t num, managed_fd* fds){ size_t u; int rv = 0; instance* inst = NULL; rtpmidi_instance_data* data = NULL; - //TODO handle mDNS discovery frames + //handle service tasks (mdns, clock sync, peer connections) + if(mm_timestamp() - cfg.last_service > RTPMIDI_SERVICE_INTERVAL){ + DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service); + rtpmidi_service(); + cfg.last_service = mm_timestamp(); + } if(!num){ return 0; @@ -866,7 +903,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ rv |= rtpmidi_handle_data(inst); } else if(fds[u].fd == data->control_fd){ - rv |= rtpmidi_handle_control(inst); + rv |= rtpmidi_handle_control(inst); } else{ LOG("Signaled for unknown descriptor"); @@ -878,7 +915,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ } static int rtpmidi_start(size_t n, instance** inst){ - size_t u, fds = 0; + size_t u, p, fds = 0; rtpmidi_instance_data* data = NULL; //if mdns name defined and no socket, bind default values @@ -920,6 +957,14 @@ static int rtpmidi_start(size_t n, instance** inst){ return 1; } + //mark configured peers on direct instances as connected so output is sent + //apple mode instances go through the session negotiation before marking peers as active + if(data->mode == direct){ + for(p = 0; p < data->peers; p++){ + data->peer[p].connected = 1; + } + } + //register fds to core if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){ LOGPF("Failed to register descriptor for instance %s with core", inst[u]->name); diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index a9effd8..db6237b 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -20,6 +20,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_HEADER_TYPE 0x61 #define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) #define RTPMIDI_DEFAULT_NAME "MIDIMonster" +#define RTPMIDI_SERVICE_INTERVAL 1000 enum /*_rtpmidi_channel_type*/ { none = 0, @@ -50,7 +51,9 @@ typedef struct /*_rtpmidi_peer*/ { struct sockaddr_storage dest; socklen_t dest_len; //uint32_t ssrc; - uint8_t inactive; + uint8_t active; //marked for reuse + uint8_t learned; //learned / configured peer + uint8_t connected; //currently in active session } rtpmidi_peer; typedef struct /*_rtmidi_instance_data*/ { -- cgit v1.2.3 From 0c21a3d8d6a6b3448d0341fbc7239f90ba888a80 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 29 Dec 2019 12:56:26 +0100 Subject: rtpmidi service handling --- backends/rtpmidi.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 6 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 0a83fee..8e2b208 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "rtpmidi" -#define DEBUG +//#define DEBUG #include #include @@ -164,6 +164,8 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag if(data->peer[u].active && sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + //if yes, update connection flag (but not learned flag because that doesnt change) + data->peer[u].connected = connected; return 0; } @@ -574,13 +576,13 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size } else if(command->command == apple_accept){ if(fd != data->control_fd){ - LOGPF("Instance %s negotiated new peer\n", inst->name); + LOGPF("Instance %s negotiated new peer", inst->name); return rtpmidi_push_peer(data, *peer, peer_len, 1, 1); //FIXME store ssrc, start timesync } else{ //send invite on data fd - LOGPF("Instance %s peer accepted on control port, inviting data port\n", inst->name); + LOGPF("Instance %s peer accepted on control port, inviting data port", inst->name); //FIXME limit max length of session name apple_command* invite = (apple_command*) response; invite->res1 = 0xFFFF; @@ -865,12 +867,70 @@ static int rtpmidi_handle_control(instance* inst){ } static int rtpmidi_service(){ + size_t n, u, p; + instance** inst = NULL; + rtpmidi_instance_data* data = NULL; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + struct sockaddr_storage control_peer; + //prepare commands + apple_sync_frame sync = { + .res1 = 0xFFFF, + .command = htobe16(apple_sync), + .ssrc = 0, + .count = 0, + .timestamp = { + mm_timestamp() * 10 + } + }; + apple_command* invite = (apple_command*) &frame; + invite->res1 = 0xFFFF; + invite->command = htobe16(apple_invite); + invite->version = htobe32(2); + invite->token = ((uint32_t) rand()) << 16 | rand(); + + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + LOG("Failed to fetch instances"); + return 1; + } + + //mdns discovery if(cfg.mdns_fd >= 0){ //TODO send applemidi discovery packets } - //TODO send sync packets for all connected applemidi peers - //TODO try to invite pre-defined unconnected applemidi peers + + for(u = 0; u < n; u++){ + data = (rtpmidi_instance_data*) inst[u]->impl; + + if(data->mode == apple){ + for(p = 0; p < data->peers; p++){ + if(data->peer[p].active && data->peer[p].connected){ + //apple sync + DBGPF("Instance %s initializing sync on peer %" PRIsize_t, inst[u]->name, p); + sync.ssrc = htobe32(data->ssrc); + //calculate remote control port from data port + memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); + ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); + + sendto(data->control_fd, &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + } + else if(data->peer[p].active && !data->peer[p].learned){ + //try to invite pre-defined unconnected applemidi peers + DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p); + invite->ssrc = htobe32(data->ssrc); + //calculate remote control port from data port + memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); + ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); + //append session name to packet + memcpy(frame + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); + + sendto(data->control_fd, invite, sizeof(apple_command) + strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + } + } + } + } + + free(inst); return 0; } @@ -883,7 +943,9 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ //handle service tasks (mdns, clock sync, peer connections) if(mm_timestamp() - cfg.last_service > RTPMIDI_SERVICE_INTERVAL){ DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service); - rtpmidi_service(); + if(rtpmidi_service()){ + return 1; + } cfg.last_service = mm_timestamp(); } -- cgit v1.2.3 From 666aec036f9bf0de82c435bd7eace271613cee1e Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 3 Jan 2020 00:25:50 +0100 Subject: Limit connection attempts to one in 10sec --- backends/rtpmidi.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 8e2b208..c39139a 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "rtpmidi" -//#define DEBUG +#define DEBUG #include #include @@ -11,8 +11,6 @@ #include "rtpmidi.h" //TODO learn peer ssrcs -//TODO participants need to initiate clock sync at some point -//TODO applemode peers still need session negotiation (peer option should only bypass discovery) //TODO default session join? //TODO default mode? //TODO internal loop mode @@ -164,7 +162,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag if(data->peer[u].active && sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ - //if yes, update connection flag (but not learned flag because that doesnt change) + //if yes, update connection flag (but not learned flag because that doesn't change) data->peer[u].connected = connected; return 0; } @@ -914,7 +912,7 @@ static int rtpmidi_service(){ sendto(data->control_fd, &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); } - else if(data->peer[p].active && !data->peer[p].learned){ + else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){ //try to invite pre-defined unconnected applemidi peers DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p); invite->ssrc = htobe32(data->ssrc); -- cgit v1.2.3 From 878988c9d5a8e106af16d55702176a0ce8f0ae71 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 9 Mar 2020 21:12:48 +0100 Subject: Remove deprecated MIDI channel syntax --- backends/midi.c | 79 +++++++++++++++++++++----------------------------------- backends/midi.md | 2 -- 2 files changed, 30 insertions(+), 51 deletions(-) (limited to 'backends') diff --git a/backends/midi.c b/backends/midi.c index f73ebb4..f378f1e 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -111,39 +111,22 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ .label = 0 }; - //support deprecated syntax for a transition period... - uint8_t old_syntax = 0; - char* channel; - + char* channel = NULL; if(!strncmp(spec, "ch", 2)){ channel = spec + 2; if(!strncmp(spec, "channel", 7)){ channel = spec + 7; } } - else if(!strncmp(spec, "cc", 2)){ - ident.fields.type = cc; - channel = spec + 2; - old_syntax = 1; - } - else if(!strncmp(spec, "note", 4)){ - ident.fields.type = note; - channel = spec + 4; - old_syntax = 1; - } - else if(!strncmp(spec, "nrpn", 4)){ - ident.fields.type = nrpn; - channel = spec + 4; - old_syntax = 1; - } - else{ - LOGPF("Unknown control type in %s", spec); + + if(!channel){ + LOGPF("Invalid channel specification %s", spec); return NULL; } ident.fields.channel = strtoul(channel, &channel, 10); if(ident.fields.channel > 15){ - LOGPF("Channel out of range in spec %s", spec); + LOGPF("MIDI channel out of range in spec %s", spec); return NULL; } @@ -154,33 +137,31 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ //skip the period channel++; - if(!old_syntax){ - if(!strncmp(channel, "cc", 2)){ - ident.fields.type = cc; - channel += 2; - } - else if(!strncmp(channel, "note", 4)){ - ident.fields.type = note; - channel += 4; - } - else if(!strncmp(channel, "nrpn", 4)){ - ident.fields.type = nrpn; - channel += 4; - } - else if(!strncmp(channel, "pressure", 8)){ - ident.fields.type = pressure; - channel += 8; - } - else if(!strncmp(channel, "pitch", 5)){ - ident.fields.type = pitchbend; - } - else if(!strncmp(channel, "aftertouch", 10)){ - ident.fields.type = aftertouch; - } - else{ - LOGPF("Unknown control type in %s", spec); - return NULL; - } + if(!strncmp(channel, "cc", 2)){ + ident.fields.type = cc; + channel += 2; + } + else if(!strncmp(channel, "note", 4)){ + ident.fields.type = note; + channel += 4; + } + else if(!strncmp(channel, "nrpn", 4)){ + ident.fields.type = nrpn; + channel += 4; + } + else if(!strncmp(channel, "pressure", 8)){ + ident.fields.type = pressure; + channel += 8; + } + else if(!strncmp(channel, "pitch", 5)){ + ident.fields.type = pitchbend; + } + else if(!strncmp(channel, "aftertouch", 10)){ + ident.fields.type = aftertouch; + } + else{ + LOGPF("Unknown control type in %s", spec); + return NULL; } ident.fields.control = strtoul(channel, NULL, 10); diff --git a/backends/midi.md b/backends/midi.md index 108860e..8188444 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -34,8 +34,6 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). -The earlier syntax of `.` is officially deprecated but still supported for compatibility -reasons. This support may be removed at some future time. The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. -- cgit v1.2.3 From 377556e3407b107d6033be246ba6f41e0681f690 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 10 Mar 2020 20:43:48 +0100 Subject: Simplify MIDI backend --- backends/midi.c | 61 ++++++++++++++++++++++++++------------------------------ backends/midi.md | 3 --- 2 files changed, 28 insertions(+), 36 deletions(-) (limited to 'backends') diff --git a/backends/midi.c b/backends/midi.c index f378f1e..1f0f2d5 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -13,9 +13,7 @@ enum /*_midi_channel_type*/ { cc, pressure, aftertouch, - pitchbend, - nrpn, - sysmsg + pitchbend }; static struct { @@ -145,10 +143,6 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ ident.fields.type = note; channel += 4; } - else if(!strncmp(channel, "nrpn", 4)){ - ident.fields.type = nrpn; - channel += 4; - } else if(!strncmp(channel, "pressure", 8)){ ident.fields.type = pressure; channel += 8; @@ -205,9 +199,6 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ case aftertouch: snd_seq_ev_set_chanpress(&ev, ident.fields.channel, v[u].normalised * 127.0); break; - case nrpn: - //FIXME set to nrpn output - break; } snd_seq_event_output(sequencer, &ev); @@ -217,6 +208,24 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ return 0; } +static char* midi_type_name(uint8_t type){ + switch(type){ + case none: + return "none"; + case note: + return "note"; + case cc: + return "cc"; + case pressure: + return "pressure"; + case aftertouch: + return "aftertouch"; + case pitchbend: + return "pitch"; + } + return "unknown"; +} + static int midi_handle(size_t num, managed_fd* fds){ snd_seq_event_t* ev = NULL; instance* inst = NULL; @@ -234,59 +243,45 @@ static int midi_handle(size_t num, managed_fd* fds){ while(snd_seq_event_input(sequencer, &ev) > 0){ event_type = NULL; ident.label = 0; + + ident.fields.channel = ev->data.note.channel; + ident.fields.control = ev->data.note.note; + val.normalised = (double) ev->data.note.velocity / 127.0; + switch(ev->type){ case SND_SEQ_EVENT_NOTEON: case SND_SEQ_EVENT_NOTEOFF: case SND_SEQ_EVENT_NOTE: ident.fields.type = note; - ident.fields.channel = ev->data.note.channel; - ident.fields.control = ev->data.note.note; - val.normalised = (double)ev->data.note.velocity / 127.0; if(ev->type == SND_SEQ_EVENT_NOTEOFF){ val.normalised = 0; } - event_type = "note"; break; case SND_SEQ_EVENT_KEYPRESS: ident.fields.type = pressure; - ident.fields.channel = ev->data.note.channel; - ident.fields.control = ev->data.note.note; - val.normalised = (double)ev->data.note.velocity / 127.0; - event_type = "pressure"; break; case SND_SEQ_EVENT_CHANPRESS: ident.fields.type = aftertouch; ident.fields.channel = ev->data.control.channel; - val.normalised = (double)ev->data.control.value / 127.0; - event_type = "aftertouch"; + val.normalised = (double) ev->data.control.value / 127.0; break; case SND_SEQ_EVENT_PITCHBEND: ident.fields.type = pitchbend; ident.fields.channel = ev->data.control.channel; - val.normalised = ((double)ev->data.control.value + 8192) / 16383.0; - event_type = "pitch"; + val.normalised = ((double) ev->data.control.value + 8192) / 16383.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.raw.u64 = ev->data.control.value; - val.normalised = (double)ev->data.control.value / 127.0; - event_type = "cc"; - break; - case SND_SEQ_EVENT_CONTROL14: - case SND_SEQ_EVENT_NONREGPARAM: - case SND_SEQ_EVENT_REGPARAM: - //FIXME value calculation - ident.fields.type = nrpn; - ident.fields.channel = ev->data.control.channel; - ident.fields.control = ev->data.control.param; + val.normalised = (double) ev->data.control.value / 127.0; break; default: LOG("Ignored event of unsupported type"); continue; } + 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 diff --git a/backends/midi.md b/backends/midi.md index 8188444..d3d6e33 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -30,7 +30,6 @@ 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 -* `nrpn` - NRPNs (not yet implemented) A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). @@ -57,7 +56,5 @@ Currently, no Note Off messages are sent (instead, Note On messages with a veloc generated, which amount to the same thing according to the spec). This may be implemented as a configuration option at a later time. -NRPNs are not yet fully implemented, though rudimentary support is in the codebase. - To see which events your MIDI devices output, ALSA provides the `aseqdump` utility. You can list all incoming events using `aseqdump -p `. -- cgit v1.2.3 From e48583db0e02336381a1626af814549bf12b1214 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 10 Mar 2020 20:44:38 +0100 Subject: Check ArtNet/sACN channel direction at map time --- backends/artnet.c | 5 +++++ backends/artnet.h | 2 +- backends/sacn.c | 5 +++++ backends/sacn.h | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 9fac332..0a1ed11 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -164,6 +164,11 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ } chan_a--; + //check output capabilities + if((flags & mmchannel_output) && !data->dest_len){ + LOGPF("Channel %s.%s mapped for output, but instance is not configured for output (missing destination)", inst->name, spec); + } + //secondary channel setup if(*spec_next == '+'){ chan_b = strtoul(spec_next + 1, NULL, 10); diff --git a/backends/artnet.h b/backends/artnet.h index d83999d..ac69b3d 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -20,7 +20,7 @@ static int artnet_shutdown(size_t n, instance** inst); #define ARTNET_KEEPALIVE_INTERVAL 1000 //limit transmit rate to at most 44 packets per second (1000/44 ~= 22) -#define ARTNET_FRAME_TIMEOUT 15 +#define ARTNET_FRAME_TIMEOUT 20 #define ARTNET_SYNTHESIZE_MARGIN 10 #define MAP_COARSE 0x0200 diff --git a/backends/sacn.c b/backends/sacn.c index 79ffb46..9c7f890 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -231,6 +231,11 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ } chan_a--; + //check output capabilities + if((flags & mmchannel_output) && !data->xmit_prio){ + LOGPF("Channel %s.%s mapped for output, but instance is not configured for output (no priority set)", inst->name, spec); + } + //if wide channel, mark fine if(*spec_next == '+'){ chan_b = strtoul(spec_next + 1, NULL, 10); diff --git a/backends/sacn.h b/backends/sacn.h index 4642e59..f2cd49f 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -16,7 +16,7 @@ static int sacn_shutdown(size_t n, instance** inst); //spec 6.6.2.1 #define SACN_KEEPALIVE_INTERVAL 1000 //spec 6.6.1 -#define SACN_FRAME_TIMEOUT 15 +#define SACN_FRAME_TIMEOUT 20 #define SACN_SYNTHESIZE_MARGIN 10 #define SACN_DISCOVERY_TIMEOUT 9000 #define SACN_PDU_MAGIC "ASC-E1.17\0\0\0" -- cgit v1.2.3 From b09f93ea146812c9a3a3a15cc7654868f9c5a468 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 10 Mar 2020 20:45:37 +0100 Subject: Restructure & simplify winmidi --- backends/winmidi.c | 57 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 24 deletions(-) (limited to 'backends') diff --git a/backends/winmidi.c b/backends/winmidi.c index ad9b02d..d9b3047 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -117,7 +117,7 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ next_token = spec + 7; } } - + if(!next_token){ LOGPF("Invalid channel specification %s", spec); return NULL; @@ -213,7 +213,7 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v midiOutShortMsg(data->device_out, output.dword); } - + return 0; } @@ -310,7 +310,7 @@ 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; - + if(ident.fields.type == 0x80){ ident.fields.type = note; val.normalised = 0; @@ -335,7 +335,6 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW case MIM_CLOSE: //device opened/closed return; - } DBGPF("Incoming message type %d channel %d control %d value %f", @@ -435,32 +434,22 @@ static int winmidi_match_output(char* prefix){ return -1; } -static int winmidi_start(size_t n, instance** inst){ - size_t p; - int device, rv = -1; - winmidi_instance_data* data = NULL; +static int winmidi_socket_pair(int* fds){ + //this really should be a size_t but getsockname specifies int* for some reason + int sockadd_len = sizeof(struct sockaddr_storage); + char* error = NULL; struct sockaddr_storage sockadd = { 0 }; - //this really should be a size_t but getsockname specifies int* for some reason - int sockadd_len = sizeof(sockadd); - char* error = NULL; - DBGPF("Main thread ID is %ld", GetCurrentThreadId()); - - //output device list if requested - if(backend_config.list_devices){ - winmidi_match_input(NULL); - winmidi_match_output(NULL); - } - //open the feedback sockets //for some reason the feedback connection fails to work on 'real' windows with ipv6 - backend_config.socket_pair[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); - if(backend_config.socket_pair[0] < 0){ + fds[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); + if(fds[0] < 0){ LOG("Failed to open feedback socket"); return 1; } - if(getsockname(backend_config.socket_pair[0], (struct sockaddr*) &sockadd, &sockadd_len)){ + + if(getsockname(fds[0], (struct sockaddr*) &sockadd, &sockadd_len)){ FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); LOGPF("Failed to query feedback socket information: %s", error); @@ -483,8 +472,8 @@ static int winmidi_start(size_t n, instance** inst){ return 1; } DBGPF("Feedback socket family %d port %d", sockadd.ss_family, be16toh(((struct sockaddr_in*)&sockadd)->sin_port)); - backend_config.socket_pair[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP); - if(backend_config.socket_pair[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){ + fds[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP); + if(fds[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){ FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); LOGPF("Failed to connect to feedback socket: %s", error); @@ -492,6 +481,26 @@ static int winmidi_start(size_t n, instance** inst){ return 1; } + return 0; +} + +static int winmidi_start(size_t n, instance** inst){ + size_t p; + int device, rv = -1; + winmidi_instance_data* data = NULL; + DBGPF("Main thread ID is %ld", GetCurrentThreadId()); + + //output device list if requested + if(backend_config.list_devices){ + winmidi_match_input(NULL); + winmidi_match_output(NULL); + } + + //open the feedback sockets + if(winmidi_socket_pair(backend_config.socket_pair)){ + return 1; + } + //set up instances and start input for(p = 0; p < n; p++){ data = (winmidi_instance_data*) inst[p]->impl; -- cgit v1.2.3 From 04494e4543150567a461dd478ce9ccdc067131b2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 10 Mar 2020 21:57:47 +0100 Subject: Move ArtNet and sACN to local channel stores --- backends/artnet.c | 29 ++++++++++++++++++----------- backends/artnet.h | 1 + backends/sacn.c | 18 +++++++++++++----- backends/sacn.h | 1 + 4 files changed, 33 insertions(+), 16 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 0a1ed11..5d26b31 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -104,12 +104,18 @@ static int artnet_configure(char* option, char* value){ static int artnet_instance(instance* inst){ artnet_instance_data* data = calloc(1, sizeof(artnet_instance_data)); + size_t u; + if(!data){ LOG("Failed to allocate memory"); return 1; } data->net = 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; + } inst->impl = data; return 0; @@ -197,7 +203,7 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ } data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a); - return mm_channel(inst, chan_a, 1); + return data->data.channel + chan_a; } static int artnet_transmit(instance* inst){ @@ -233,7 +239,7 @@ static int artnet_transmit(instance* inst){ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v){ uint32_t frame_delta = 0; - size_t u, mark = 0; + size_t u, mark = 0, channel_offset = 0; artnet_instance_data* data = (artnet_instance_data*) inst->impl; if(!data->dest_len){ @@ -242,22 +248,23 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) } for(u = 0; u < num; u++){ - if(IS_WIDE(data->data.map[c[u]->ident])){ + channel_offset = c[u]->ident; + if(IS_WIDE(data->data.map[channel_offset])){ uint32_t val = v[u].normalised * ((double) 0xFFFF); //the primary (coarse) channel is the one registered to the core, so we don't have to check for that - if(data->data.out[c[u]->ident] != ((val >> 8) & 0xFF)){ + if(data->data.out[channel_offset] != ((val >> 8) & 0xFF)){ mark = 1; - data->data.out[c[u]->ident] = (val >> 8) & 0xFF; + data->data.out[channel_offset] = (val >> 8) & 0xFF; } - if(data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){ + if(data->data.out[MAPPED_CHANNEL(data->data.map[channel_offset])] != (val & 0xFF)){ mark = 1; - data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF; + data->data.out[MAPPED_CHANNEL(data->data.map[channel_offset])] = val & 0xFF; } } - else if(data->data.out[c[u]->ident] != (v[u].normalised * 255.0)){ + else if(data->data.out[channel_offset] != (v[u].normalised * 255.0)){ mark = 1; - data->data.out[c[u]->ident] = v[u].normalised * 255.0; + data->data.out[channel_offset] = v[u].normalised * 255.0; } } @@ -310,10 +317,10 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ if(data->data.map[p] & MAP_MARK){ data->data.map[p] &= ~MAP_MARK; if(data->data.map[p] & MAP_FINE){ - chan = mm_channel(inst, MAPPED_CHANNEL(data->data.map[p]), 0); + chan = data->data.channel + MAPPED_CHANNEL(data->data.map[p]); } else{ - chan = mm_channel(inst, p, 0); + chan = data->data.channel + p; } if(!chan){ diff --git a/backends/artnet.h b/backends/artnet.h index ac69b3d..a517aa0 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -37,6 +37,7 @@ typedef struct /*_artnet_universe_model*/ { uint8_t in[512]; uint8_t out[512]; uint16_t map[512]; + channel channel[512]; } artnet_universe; typedef struct /*_artnet_instance_model*/ { diff --git a/backends/sacn.c b/backends/sacn.c index 9c7f890..dd05dc7 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -209,12 +209,20 @@ static int sacn_configure_instance(instance* inst, char* option, char* value){ } static int sacn_instance(instance* inst){ - inst->impl = calloc(1, sizeof(sacn_instance_data)); - if(!inst->impl){ + sacn_instance_data* data = calloc(1, sizeof(sacn_instance_data)); + size_t u; + + if(!data){ LOG("Failed to allocate memory"); return 1; } + for(u = 0; u < sizeof(data->data.channel) / sizeof(channel); u++){ + data->data.channel[u].ident = u; + data->data.channel[u].instance = inst; + } + + inst->impl = data; return 0; } @@ -264,7 +272,7 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ } data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a); - return mm_channel(inst, chan_a, 1); + return data->data.channel + chan_a; } static int sacn_transmit(instance* inst){ @@ -424,10 +432,10 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame //unmark and get channel inst_data->data.map[u] &= ~MAP_MARK; if(inst_data->data.map[u] & MAP_FINE){ - chan = mm_channel(inst, MAPPED_CHANNEL(inst_data->data.map[u]), 0); + chan = inst_data->data.channel + MAPPED_CHANNEL(inst_data->data.map[u]); } else{ - chan = mm_channel(inst, u, 0); + chan = inst_data->data.channel + u; } if(!chan){ diff --git a/backends/sacn.h b/backends/sacn.h index f2cd49f..4138f45 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -36,6 +36,7 @@ typedef struct /*_sacn_universe_model*/ { uint8_t in[512]; uint8_t out[512]; uint16_t map[512]; + channel channel[512]; } sacn_universe; typedef struct /*_sacn_instance_model*/ { -- cgit v1.2.3 From 678cc465124ad81dcec47c44cc30827e5246bd3b Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 11 Mar 2020 20:50:29 +0100 Subject: Simplify ArtNet & sACN --- backends/artnet.c | 4 +--- backends/artnet.md | 2 ++ backends/sacn.c | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 5d26b31..7d5d9ee 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -316,12 +316,10 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ for(p = 0; p <= max_mark; p++){ if(data->data.map[p] & MAP_MARK){ data->data.map[p] &= ~MAP_MARK; + chan = data->data.channel + p; if(data->data.map[p] & MAP_FINE){ chan = data->data.channel + MAPPED_CHANNEL(data->data.map[p]); } - else{ - chan = data->data.channel + p; - } if(!chan){ LOGPF("Active channel %" PRIsize_t " on %s not known to core", p, inst->name); diff --git a/backends/artnet.md b/backends/artnet.md index 7e1ecff..383203d 100644 --- a/backends/artnet.md +++ b/backends/artnet.md @@ -3,6 +3,8 @@ The ArtNet backend provides read-write access to the UDP-based ArtNet protocol for lighting fixture control. +Art-Netâ„¢ Designed by and Copyright Artistic Licence Holdings Ltd. + #### Global configuration | Option | Example value | Default value | Description | diff --git a/backends/sacn.c b/backends/sacn.c index dd05dc7..c9be8ff 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -431,12 +431,10 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame if(inst_data->data.map[u] & MAP_MARK){ //unmark and get channel inst_data->data.map[u] &= ~MAP_MARK; + chan = inst_data->data.channel + u; if(inst_data->data.map[u] & MAP_FINE){ chan = inst_data->data.channel + MAPPED_CHANNEL(inst_data->data.map[u]); } - else{ - chan = inst_data->data.channel + u; - } if(!chan){ LOGPF("Active channel %" PRIsize_t " on %s not known to core", u, inst->name); -- cgit v1.2.3 From 4b120a64f68fd7f36e8080981d68d0830d113205 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 12 Mar 2020 21:51:43 +0100 Subject: Fix rate-limited frame synthesis --- backends/artnet.c | 4 ++-- backends/sacn.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 7d5d9ee..34fc82d 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -280,8 +280,8 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) //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_KEEPALIVE_INTERVAL - frame_delta)){ - next_frame = (ARTNET_KEEPALIVE_INTERVAL - frame_delta); + if(!next_frame || next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){ + next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta); } return 0; } diff --git a/backends/sacn.c b/backends/sacn.c index c9be8ff..495bdf3 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -374,8 +374,8 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ //check if ratelimiting engaged if(frame_delta < SACN_FRAME_TIMEOUT){ global_cfg.fd[data->fd_index].universe[u].mark = 1; - if(!global_cfg.next_frame || global_cfg.next_frame > (SACN_KEEPALIVE_INTERVAL - frame_delta)){ - global_cfg.next_frame = (SACN_KEEPALIVE_INTERVAL - frame_delta); + if(!global_cfg.next_frame || global_cfg.next_frame > (SACN_FRAME_TIMEOUT - frame_delta)){ + global_cfg.next_frame = (SACN_FRAME_TIMEOUT - frame_delta); } return 0; } -- cgit v1.2.3 From 05cdf563fcc4c7835ec422fa5d7ee86b68a9f1df Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 14 Mar 2020 21:45:09 +0100 Subject: Restructure backend and instance registry --- backends/artnet.c | 5 ----- backends/sacn.c | 5 ----- 2 files changed, 10 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 34fc82d..caab6e0 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -321,11 +321,6 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ chan = data->data.channel + MAPPED_CHANNEL(data->data.map[p]); } - if(!chan){ - LOGPF("Active channel %" PRIsize_t " on %s not known to core", p, inst->name); - return 1; - } - if(IS_WIDE(data->data.map[p])){ data->data.map[MAPPED_CHANNEL(data->data.map[p])] &= ~MAP_MARK; wide_val = data->data.in[p] << ((data->data.map[p] & MAP_COARSE) ? 8 : 0); diff --git a/backends/sacn.c b/backends/sacn.c index 495bdf3..bd5c75a 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -436,11 +436,6 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame chan = inst_data->data.channel + MAPPED_CHANNEL(inst_data->data.map[u]); } - if(!chan){ - LOGPF("Active channel %" PRIsize_t " on %s not known to core", u, inst->name); - return 1; - } - //generate value if(IS_WIDE(inst_data->data.map[u])){ inst_data->data.map[MAPPED_CHANNEL(inst_data->data.map[u])] &= ~MAP_MARK; -- cgit v1.2.3 From 636a9592eab1963c2b1a77bd59eaff317fa55b75 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Mar 2020 01:09:52 +0100 Subject: Fix registry apply bug, fix openpixelcontrol broadcast --- backends/openpixelcontrol.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 168e077..2a5e01f 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -495,11 +495,11 @@ static ssize_t openpixel_client_headerdata(instance* inst, openpixel_client* cli } else{ client->buffer = openpixel_buffer_find(data, client->hdr.strip, 1); - } - //if no buffer or mode mismatch, ignore data - if(client->buffer < 0 - || data->mode != client->hdr.mode){ - client->buffer = -2; //mark for ignore + //if no buffer or mode mismatch, ignore data + if(client->buffer < 0 + || data->mode != client->hdr.mode){ + client->buffer = -2; //mark for ignore + } } client->left = be16toh(client->hdr.length); client->offset = 0; -- cgit v1.2.3 From 9718e10c7f4151cea895f515c785c14e0021d967 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 18 Mar 2020 22:36:35 +0100 Subject: Implement default channel handlers for Lua/Python --- backends/lua.c | 17 +++++++++++++- backends/lua.h | 2 ++ backends/lua.md | 10 +++++---- backends/python.c | 65 +++++++++++++++++++++++++++++++++++++----------------- backends/python.h | 3 +++ backends/python.md | 8 ++++--- 6 files changed, 77 insertions(+), 28 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index e7ba9f9..498a037 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -308,6 +308,11 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ } return 0; } + else if(!strcmp(option, "default-handler")){ + free(data->default_handler); + data->default_handler = strdup(value); + return 0; + } LOGPF("Unknown instance configuration parameter %s for instance %s", option, inst->name); return 1; @@ -461,7 +466,8 @@ static int lua_start(size_t n, instance** inst){ data = (lua_instance_data*) inst[u]->impl; for(p = 0; p < data->channels; p++){ //exclude reserved names - if(strcmp(data->channel_name[p], "output") + if(!data->default_handler + && strcmp(data->channel_name[p], "output") && strcmp(data->channel_name[p], "input_value") && strcmp(data->channel_name[p], "output_value") && strcmp(data->channel_name[p], "input_channel") @@ -473,6 +479,14 @@ static int lua_start(size_t n, instance** inst){ data->reference[p] = LUA_NOREF; } } + else if(data->default_handler){ + lua_getglobal(data->interpreter, data->default_handler); + data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); + if(data->reference[p] == LUA_REFNIL){ + data->reference[p] = LUA_NOREF; + LOGPF("Failed to resolve default handler function %s on instance %s", data->default_handler, inst[u]->name); + } + } } } @@ -504,6 +518,7 @@ static int lua_shutdown(size_t n, instance** inst){ free(data->reference); free(data->input); free(data->output); + free(data->default_handler); free(inst[u]->impl); } diff --git a/backends/lua.h b/backends/lua.h index ebe2046..743f978 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -29,6 +29,8 @@ typedef struct /*_lua_instance_data*/ { double* input; double* output; lua_State* interpreter; + + char* default_handler; } lua_instance_data; typedef struct /*_lua_interval_callback*/ { diff --git a/backends/lua.md b/backends/lua.md index db4cf39..96e53c8 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -6,7 +6,8 @@ and manipulate events using the Lua scripting language. Every instance has its own interpreter state which can be loaded with custom handler scripts. To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) -with the value (as a Lua `number` type) as parameter. +with the value (as a Lua `number` type) as parameter. Alternatively, a designated default channel handler +may be supplied in the configuration. The following functions are provided within the Lua interpreter for interaction with the MIDIMonster @@ -42,9 +43,10 @@ The `lua` backend does not take any global configuration. #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `script` | `script.lua` | none | Lua source file (relative to configuration file)| +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------| +| `script` | `script.lua` | none | Lua source file (relative to configuration file) | +| `default-handler` | `handler` | none | Name of a function to be called as handler for all incoming channels (instead of the per-channel handlers) | A single instance may have multiple `script` options specified, which will all be read cumulatively. diff --git a/backends/python.c b/backends/python.c index 70c2548..735e838 100644 --- a/backends/python.c +++ b/backends/python.c @@ -378,6 +378,11 @@ static int python_configure_instance(instance* inst, char* option, char* value){ PyEval_ReleaseThread(data->interpreter); return 0; } + else if(!strcmp(option, "default-handler")){ + free(data->default_handler); + data->default_handler = strdup(value); + return 0; + } LOGPF("Unknown instance parameter %s for instance %s", option, inst->name); return 1; @@ -600,38 +605,56 @@ static int python_handle(size_t num, managed_fd* fds){ return 0; } +static PyObject* python_resolve_symbol(char* spec_raw){ + char* module_name = NULL, *object_name = NULL, *spec = strdup(spec_raw); + PyObject* module = NULL, *result = NULL; + + module = PyImport_AddModule("__main__"); + object_name = spec; + module_name = strchr(object_name, '.'); + if(module_name){ + *module_name = 0; + //returns borrowed reference + module = PyImport_AddModule(object_name); + + if(!module){ + LOGPF("Module %s for symbol %s.%s is not loaded", object_name, object_name, module_name + 1); + return NULL; + } + + object_name = module_name + 1; + + //returns new reference + result = PyObject_GetAttrString(module, object_name); + } + + free(spec); + return result; +} + static int python_start(size_t n, instance** inst){ python_instance_data* data = NULL; - PyObject* module = NULL; size_t u, p; - char* module_name = NULL, *channel_name = NULL; //resolve channel references to handler functions for(u = 0; u < n; u++){ data = (python_instance_data*) inst[u]->impl; + DBGPF("Starting up instance %s", inst[u]->name); //switch to interpreter PyEval_RestoreThread(data->interpreter); - for(p = 0; p < data->channels; p++){ - module = PyImport_AddModule("__main__"); - channel_name = data->channel[p].name; - module_name = strchr(channel_name, '.'); - if(module_name){ - *module_name = 0; - //returns borrowed reference - module = PyImport_AddModule(channel_name); - - if(!module){ - LOGPF("Module %s for qualified channel %s.%s is not loaded on instance %s", channel_name, channel_name, module_name + 1, inst[u]->name); - return 1; - } - *module_name = '.'; - channel_name = module_name + 1; - } + if(data->default_handler){ + data->handler = python_resolve_symbol(data->default_handler); + } - //returns new reference - data->channel[p].handler = PyObject_GetAttrString(module, channel_name); + for(p = 0; p < data->channels; p++){ + if(!strchr(data->channel[p].name, '.') && data->handler){ + data->channel[p].handler = data->handler; + } + else{ + data->channel[p].handler = python_resolve_symbol(data->channel[p].name); + } } //release interpreter @@ -654,6 +677,7 @@ static int python_shutdown(size_t n, instance** inst){ Py_XDECREF(data->channel[p].handler); } free(data->channel); + free(data->default_handler); //do not free data here, needed for shutting down interpreters } @@ -675,6 +699,7 @@ static int python_shutdown(size_t n, instance** inst){ for(p = 0; p handler); DBGPF("Shutting down interpreter for instance %s", inst[u]->name); //swap to interpreter and end it, GIL is held after this but state is NULL diff --git a/backends/python.h b/backends/python.h index 8ca12f9..a40098b 100644 --- a/backends/python.h +++ b/backends/python.h @@ -41,4 +41,7 @@ typedef struct /*_python_instance_data*/ { size_t channels; mmpython_channel* channel; mmpython_channel* current_channel; + + char* default_handler; + PyObject* handler; } python_instance_data; diff --git a/backends/python.md b/backends/python.md index f06e504..6852a79 100644 --- a/backends/python.md +++ b/backends/python.md @@ -6,6 +6,7 @@ to route, generate and manipulate channel events using the Python 3 scripting la Every instance has its own interpreter, which can be loaded with multiple Python modules. These modules may contain member functions accepting a single `float` parameter, which can then be used as target channels. For each incoming event, the handler function is called. +Channels in the global scope may be assigned a default handler function. Python modules may also register `socket` objects (and an associated callback function) with the MIDIMonster core, which will then alert the module when there is data ready to be read. @@ -67,9 +68,10 @@ The `python` backend does not take any global configuration. #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------------------------------| -| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location | +| 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) | 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). -- cgit v1.2.3 From 2d66d5cee9bf3ed5779f65d8a99b40ee5181bf30 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 20 Mar 2020 21:50:33 +0100 Subject: Implement Lua threading --- backends/lua.c | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- backends/lua.h | 7 ++++ backends/lua.md | 30 ++++++++++++--- 3 files changed, 144 insertions(+), 7 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 498a037..b66a27a 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -10,6 +10,7 @@ #define LUA_REGISTRY_KEY "_midimonster_lua_instance" #define LUA_REGISTRY_CURRENT_CHANNEL "_midimonster_lua_channel" +#define LUA_REGISTRY_CURRENT_THREAD "_midimonster_lua_thread" static size_t timers = 0; static lua_timer* timer = NULL; @@ -20,6 +21,9 @@ static int timer_fd = -1; static uint64_t last_timestamp; #endif +static size_t threads = 0; +static lua_thread* thread = NULL; + MM_PLUGIN_API int init(){ backend lua = { #ifndef MMBACKEND_LUA_TIMERFD @@ -86,6 +90,12 @@ static int lua_update_timerfd(){ interval = timer[n].interval; } } + + for(n = 0; n < threads; n++){ + if(thread[n].timeout && (!interval || thread[n].timeout < interval)){ + interval = thread[n].timeout; + } + } DBGPF("Recalculating timers, minimum is %" PRIu64, interval); //calculate gcd of all timers if any are active @@ -100,7 +110,8 @@ static int lua_update_timerfd(){ gcd = residual; } //since we round everything, 10 is the lowest interval we get - if(interval == 10){ + if(interval <= 10){ + interval = 10; break; } } @@ -126,6 +137,89 @@ static int lua_update_timerfd(){ return 0; } +static void lua_thread_resume(size_t current_thread){ + //push coroutine reference + lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD); + lua_pushnumber(thread[current_thread].thread, current_thread); + lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); + + //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); + thread[current_thread].timeout = 0; + } + + //remove coroutine reference + lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD); + lua_pushnil(thread[current_thread].thread); + lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); +} + +static int lua_callback_thread(lua_State* interpreter){ + instance* inst = NULL; + size_t u = threads; + if(lua_gettop(interpreter) != 1){ + LOGPF("Thread function called with %d arguments, expected function", lua_gettop(interpreter)); + return 0; + } + + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + + //make space for a new thread + thread = realloc(thread, (threads + 1) * sizeof(lua_thread)); + if(!thread){ + threads = 0; + LOG("Failed to allocate memory"); + return 0; + } + threads++; + + thread[u].thread = lua_newthread(interpreter); + thread[u].instance = inst; + thread[u].timeout = 0; + thread[u].reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + + DBGPF("Registered thread %" PRIsize_t " on %s", threads, inst->name); + + //push thread main + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + lua_pushvalue(interpreter, 1); + lua_xmove(interpreter, thread[u].thread, 1); + + lua_thread_resume(u); + lua_update_timerfd(); + return 0; +} + +static int lua_callback_sleep(lua_State* interpreter){ + uint64_t timeout = 0; + size_t current_thread = threads; + if(lua_gettop(interpreter) != 1){ + LOGPF("Sleep function called with %d arguments, expected number", lua_gettop(interpreter)); + return 0; + } + + timeout = luaL_checkinteger(interpreter, 1); + + lua_pushstring(interpreter, LUA_REGISTRY_CURRENT_THREAD); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + + current_thread = luaL_checkinteger(interpreter, -1); + + if(current_thread < threads){ + DBGPF("Yielding for %" PRIu64 "msec on thread %" PRIsize_t, timeout, current_thread); + thread[current_thread].timeout = timeout; + lua_yield(interpreter, 0); + } + return 0; +} + static int lua_callback_output(lua_State* interpreter){ size_t n = 0; channel_value val; @@ -341,6 +435,8 @@ static int lua_instance(instance* inst){ lua_register(data->interpreter, "output_value", lua_callback_output_value); lua_register(data->interpreter, "input_channel", lua_callback_input_channel); lua_register(data->interpreter, "timestamp", lua_callback_timestamp); + lua_register(data->interpreter, "thread", lua_callback_thread); + lua_register(data->interpreter, "sleep", lua_callback_sleep); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -454,6 +550,17 @@ static int lua_handle(size_t num, managed_fd* fds){ } } } + + //check for threads to wake up + for(n = 0; n < threads; n++){ + if(thread[n].timeout && delta >= thread[n].timeout){ + lua_thread_resume(n); + lua_update_timerfd(); + } + else if(thread[n].timeout){ + thread[n].timeout -= delta; + } + } return 0; } @@ -468,6 +575,8 @@ static int lua_start(size_t n, instance** inst){ //exclude reserved names if(!data->default_handler && strcmp(data->channel_name[p], "output") + && strcmp(data->channel_name[p], "thread") + && strcmp(data->channel_name[p], "sleep") && strcmp(data->channel_name[p], "input_value") && strcmp(data->channel_name[p], "output_value") && strcmp(data->channel_name[p], "input_channel") @@ -526,6 +635,9 @@ static int lua_shutdown(size_t n, instance** inst){ free(timer); timer = NULL; timers = 0; + free(thread); + thread = NULL; + threads = 0; #ifdef MMBACKEND_LUA_TIMERFD close(timer_fd); timer_fd = -1; diff --git a/backends/lua.h b/backends/lua.h index 743f978..d8e720a 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -39,3 +39,10 @@ typedef struct /*_lua_interval_callback*/ { lua_State* interpreter; int reference; } lua_timer; + +typedef struct /*_lua_coroutine*/ { + instance* instance; + lua_State* thread; + int reference; + uint64_t timeout; +} lua_thread; diff --git a/backends/lua.md b/backends/lua.md index 96e53c8..05509b6 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -19,24 +19,41 @@ The following functions are provided within the Lua interpreter for interaction | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | | `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 | Example script: -``` +```lua function bar(value) - output("foo", value / 2) + output("foo", value / 2); end step = 0 function toggle() - output("bar", step * 1.0) + output("bar", step * 1.0); step = (step + 1) % 2; end +function run_show() + while(true) do + sleep(1000); + output("narf", 0); + sleep(1000); + output("narf", 1.0); + end +end + interval(toggle, 1000) +thread(run_show) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. +Threads are implemented as Lua coroutines, not operating system threads. This means that +cooperative multithreading is required, which can be achieved by calling the `sleep(number)` +function from within a running thread. Calling that function from any other context is +not supported. + #### Global configuration The `lua` backend does not take any global configuration. @@ -61,9 +78,10 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`, -`timestamp`) as an input channel name to a Lua instance will not call any handler functions. -Using these names as arguments to the output and value interface functions works as intended. +Using any of the interface functions (`output`, `interval`, etc.) as an input channel name to a +Lua instance will not call any handler functions. Using these names as arguments to the output and +value interface functions works as intended. When using a default handler, the default handler will +be called. Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. This is intentional. -- cgit v1.2.3 From 03a75ea3b4e72f09843bc8b12f8bac84a2f27709 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 20 Mar 2020 23:35:53 +0100 Subject: Restructure lua channel resolution --- backends/lua.c | 105 +++++++++++++++++++++++++++++------------------------- backends/lua.h | 14 +++++--- backends/python.c | 3 +- 3 files changed, 68 insertions(+), 54 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index b66a27a..b71ca97 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -245,13 +245,13 @@ static int lua_callback_output(lua_State* interpreter){ //find correct channel & output value for(n = 0; n < data->channels; n++){ - if(!strcmp(channel_name, data->channel_name[n])){ + if(!strcmp(channel_name, data->channel[n].name)){ channel = mm_channel(inst, n, 0); if(!channel){ return 0; } mm_channel_event(channel, val); - data->output[n] = val.normalised; + data->channel[n].out = val.normalised; return 0; } } @@ -357,8 +357,8 @@ static int lua_callback_value(lua_State* interpreter, uint8_t input){ //find correct channel & return value for(n = 0; n < data->channels; n++){ - if(!strcmp(channel_name, data->channel_name[n])){ - lua_pushnumber(interpreter, (input) ? data->input[n] : data->output[n]); + if(!strcmp(channel_name, data->channel[n].name)){ + lua_pushnumber(interpreter, (input) ? data->channel[n].in : data->channel[n].out); return 1; } } @@ -453,26 +453,23 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ //find matching channel for(u = 0; u < data->channels; u++){ - if(!strcmp(spec, data->channel_name[u])){ + if(!strcmp(spec, data->channel[u].name)){ break; } } //allocate new channel if(u == data->channels){ - data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*)); - data->reference = realloc(data->reference, (u + 1) * sizeof(int)); - data->input = realloc(data->input, (u + 1) * sizeof(double)); - data->output = realloc(data->output, (u + 1) * sizeof(double)); - if(!data->channel_name || !data->reference || !data->input || !data->output){ + data->channel = realloc(data->channel, (data->channels + 1) * sizeof(lua_channel_data)); + if(!data->channel){ LOG("Failed to allocate memory"); + data->channels = 0; return NULL; } - data->reference[u] = LUA_NOREF; - data->input[u] = data->output[u] = 0.0; - data->channel_name[u] = strdup(spec); - if(!data->channel_name[u]){ + data->channel[u].in = data->channel[u].out = 0.0; + data->channel[u].name = strdup(spec); + if(!data->channel[u].name){ LOG("Failed to allocate memory"); return NULL; } @@ -483,23 +480,24 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ } static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ - size_t n = 0; + size_t n = 0, ident; lua_instance_data* data = (lua_instance_data*) inst->impl; //handle all incoming events for(n = 0; n < num; n++){ - data->input[c[n]->ident] = v[n].normalised; + ident = c[n]->ident; + data->channel[ident].in = v[n].normalised; //call lua channel handlers if present - if(data->reference[c[n]->ident] != LUA_NOREF){ + if(data->channel[ident].reference != LUA_NOREF){ //push the channel name lua_pushstring(data->interpreter, LUA_REGISTRY_CURRENT_CHANNEL); - lua_pushstring(data->interpreter, data->channel_name[c[n]->ident]); + lua_pushstring(data->interpreter, data->channel[ident].name); lua_settable(data->interpreter, LUA_REGISTRYINDEX); - lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->channel[ident].reference); lua_pushnumber(data->interpreter, v[n].normalised); if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ - LOGPF("Failed to call handler for %s.%s: %s", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); + LOGPF("Failed to call handler for %s.%s: %s", inst->name, data->channel[ident].name, lua_tostring(data->interpreter, -1)); lua_pop(data->interpreter, 1); } } @@ -564,37 +562,51 @@ static int lua_handle(size_t num, managed_fd* fds){ return 0; } +static int lua_resolve_symbol(lua_State* interpreter, char* symbol){ + int reference = LUA_REFNIL; + + //exclude reserved names + if(!strcmp(symbol, "output") + || !strcmp(symbol, "thread") + || !strcmp(symbol, "sleep") + || !strcmp(symbol, "input_value") + || !strcmp(symbol, "output_value") + || !strcmp(symbol, "input_channel") + || !strcmp(symbol, "timestamp") + || !strcmp(symbol, "interval")){ + return LUA_NOREF; + } + + lua_getglobal(interpreter, symbol); + reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + if(reference == LUA_REFNIL){ + return LUA_NOREF; + } + return reference; +} + static int lua_start(size_t n, instance** inst){ size_t u, p; lua_instance_data* data = NULL; + int default_handler; //resolve channels to their handler functions for(u = 0; u < n; u++){ data = (lua_instance_data*) inst[u]->impl; - for(p = 0; p < data->channels; p++){ - //exclude reserved names - if(!data->default_handler - && strcmp(data->channel_name[p], "output") - && strcmp(data->channel_name[p], "thread") - && strcmp(data->channel_name[p], "sleep") - && strcmp(data->channel_name[p], "input_value") - && strcmp(data->channel_name[p], "output_value") - && strcmp(data->channel_name[p], "input_channel") - && strcmp(data->channel_name[p], "timestamp") - && strcmp(data->channel_name[p], "interval")){ - lua_getglobal(data->interpreter, data->channel_name[p]); - data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); - if(data->reference[p] == LUA_REFNIL){ - data->reference[p] = LUA_NOREF; - } + default_handler = LUA_NOREF; + + //try to resolve default handler if given + if(data->default_handler){ + default_handler = lua_resolve_symbol(data->interpreter, data->default_handler); + if(default_handler == LUA_NOREF){ + LOGPF("Failed to resolve default handler %s on %s", data->default_handler, inst[u]->name); } - else if(data->default_handler){ - lua_getglobal(data->interpreter, data->default_handler); - data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); - if(data->reference[p] == LUA_REFNIL){ - data->reference[p] = LUA_NOREF; - LOGPF("Failed to resolve default handler function %s on instance %s", data->default_handler, inst[u]->name); - } + } + + for(p = 0; p < data->channels; p++){ + data->channel[p].reference = default_handler; + if(!data->default_handler){ + data->channel[p].reference = lua_resolve_symbol(data->interpreter, data->channel[p].name); } } } @@ -621,12 +633,9 @@ static int lua_shutdown(size_t n, instance** inst){ lua_close(data->interpreter); //cleanup channel data for(p = 0; p < data->channels; p++){ - free(data->channel_name[p]); + free(data->channel[p].name); } - free(data->channel_name); - free(data->reference); - free(data->input); - free(data->output); + free(data->channel); free(data->default_handler); free(inst[u]->impl); } diff --git a/backends/lua.h b/backends/lua.h index d8e720a..3c7568b 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -22,14 +22,18 @@ static int lua_shutdown(size_t n, instance** inst); static uint32_t lua_interval(); #endif +typedef struct /*_lua_channel*/ { + char* name; + int reference; + double in; + double out; +} lua_channel_data; + typedef struct /*_lua_instance_data*/ { size_t channels; - char** channel_name; - int* reference; - double* input; - double* output; - lua_State* interpreter; + lua_channel_data* channel; + lua_State* interpreter; char* default_handler; } lua_instance_data; diff --git a/backends/python.c b/backends/python.c index 735e838..c658f30 100644 --- a/backends/python.c +++ b/backends/python.c @@ -78,7 +78,8 @@ static void python_timer_recalculate(){ } //10msec is absolute lower limit and minimum gcd due to rounding - if(next_interval == 10){ + if(next_interval <= 10){ + next_interval = 10; break; } } -- cgit v1.2.3 From d1baab6ce4b13c562867147ed60906dfe651ae81 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 21 Mar 2020 19:00:28 +0100 Subject: Enable setting channel values at load time for lua --- backends/lua.c | 29 +++++++++++++++++++---------- backends/lua.h | 1 + 2 files changed, 20 insertions(+), 10 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index b71ca97..7f80cc7 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -17,9 +17,8 @@ static lua_timer* timer = NULL; uint64_t timer_interval = 0; #ifdef MMBACKEND_LUA_TIMERFD static int timer_fd = -1; -#else -static uint64_t last_timestamp; #endif +static uint64_t last_timestamp = 0; static size_t threads = 0; static lua_thread* thread = NULL; @@ -224,7 +223,6 @@ static int lua_callback_output(lua_State* interpreter){ size_t n = 0; channel_value val; const char* channel_name = NULL; - channel* channel = NULL; instance* inst = NULL; lua_instance_data* data = NULL; @@ -243,15 +241,21 @@ static int lua_callback_output(lua_State* interpreter){ channel_name = lua_tostring(interpreter, 1); val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); + //if not started yet, create any requested channels so scripts may set them at load time + if(!last_timestamp && channel_name){ + lua_channel(inst, (char*) channel_name, mmchannel_output); + } + //find correct channel & output value for(n = 0; n < data->channels; n++){ if(!strcmp(channel_name, data->channel[n].name)){ - channel = mm_channel(inst, n, 0); - if(!channel){ - return 0; - } - mm_channel_event(channel, val); data->channel[n].out = val.normalised; + if(!last_timestamp){ + data->channel[n].mark = 1; + } + else{ + mm_channel_event(mm_channel(inst, n, 0), val); + } return 0; } } @@ -589,6 +593,7 @@ static int lua_start(size_t n, instance** inst){ size_t u, p; lua_instance_data* data = NULL; int default_handler; + channel_value v; //resolve channels to their handler functions for(u = 0; u < n; u++){ @@ -608,6 +613,11 @@ static int lua_start(size_t n, instance** inst){ if(!data->default_handler){ data->channel[p].reference = lua_resolve_symbol(data->interpreter, data->channel[p].name); } + //push initial values + if(data->channel[p].mark){ + v.normalised = data->channel[p].out; + mm_channel_event(mm_channel(inst[u], p, 0), v); + } } } @@ -617,9 +627,8 @@ static int lua_start(size_t n, instance** inst){ if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ return 1; } - #else - last_timestamp = mm_timestamp(); #endif + last_timestamp = mm_timestamp(); return 0; } diff --git a/backends/lua.h b/backends/lua.h index 3c7568b..4583dfe 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -27,6 +27,7 @@ typedef struct /*_lua_channel*/ { int reference; double in; double out; + uint8_t mark; } lua_channel_data; typedef struct /*_lua_instance_data*/ { -- cgit v1.2.3 From aa02ccf3abf183207b24fcbb9460cbd904a698e2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 22 Mar 2020 00:30:13 +0100 Subject: Enable load-time channel setting for python --- backends/python.c | 26 +++++++++++++++++--------- backends/python.h | 1 + 2 files changed, 18 insertions(+), 9 deletions(-) (limited to 'backends') diff --git a/backends/python.c b/backends/python.c index c658f30..9f1d642 100644 --- a/backends/python.c +++ b/backends/python.c @@ -116,7 +116,6 @@ static PyObject* mmpy_output(PyObject* self, PyObject* args){ instance* inst = *((instance**) PyModule_GetState(self)); python_instance_data* data = (python_instance_data*) inst->impl; const char* channel_name = NULL; - channel* chan = NULL; channel_value val = { {0} }; @@ -127,19 +126,22 @@ static PyObject* mmpy_output(PyObject* self, PyObject* args){ } val.normalised = clamp(val.normalised, 1.0, 0.0); + //if not started yet, create any requested channels so we can set them at load time + if(!last_timestamp){ + python_channel(inst, (char*) channel_name, mmchannel_output); + } for(u = 0; u < data->channels; u++){ if(!strcmp(data->channel[u].name, channel_name)){ DBGPF("Setting channel %s.%s to %f", inst->name, channel_name, val.normalised); - chan = mm_channel(inst, u, 0); - //this should never happen - if(!chan){ - LOGPF("Failed to fetch parsed channel %s.%s", inst->name, channel_name); - break; - } data->channel[u].out = val.normalised; - mm_channel_event(chan, val); - break; + if(!last_timestamp){ + data->channel[u].mark = 1; + } + else{ + mm_channel_event(mm_channel(inst, u, 0), val); + } + return 0; } } @@ -636,6 +638,7 @@ static PyObject* python_resolve_symbol(char* spec_raw){ static int python_start(size_t n, instance** inst){ python_instance_data* data = NULL; size_t u, p; + channel_value v; //resolve channel references to handler functions for(u = 0; u < n; u++){ @@ -656,6 +659,11 @@ static int python_start(size_t n, instance** inst){ else{ data->channel[p].handler = python_resolve_symbol(data->channel[p].name); } + //push initial values + if(data->channel[p].mark){ + v.normalised = data->channel[p].out; + mm_channel_event(mm_channel(inst[u], p, 0), v); + } } //release interpreter diff --git a/backends/python.h b/backends/python.h index a40098b..020aeac 100644 --- a/backends/python.h +++ b/backends/python.h @@ -16,6 +16,7 @@ typedef struct /*_python_channel_data*/ { PyObject* handler; double in; double out; + uint8_t mark; } mmpython_channel; typedef struct /*_mmpy_registered_socket*/ { -- cgit v1.2.3 From e8d87602618b4a9fab056f1d03b87c706d5f1f9f Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 23 Mar 2020 01:30:14 +0100 Subject: Update rtpmidi to current API --- backends/Makefile | 2 +- backends/rtpmidi.c | 23 ++++++----------------- backends/rtpmidi.h | 2 +- 3 files changed, 8 insertions(+), 19 deletions(-) (limited to 'backends') diff --git a/backends/Makefile b/backends/Makefile index 8956a20..c19d9dd 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.so WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index c39139a..2eec871 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -357,24 +357,17 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 1; } -static instance* rtpmidi_instance(){ - rtpmidi_instance_data* data = NULL; - instance* inst = mm_instance(); - - if(!inst){ - return NULL; - } - - data = calloc(1, sizeof(rtpmidi_instance_data)); +static int rtpmidi_instance(instance* inst){ + rtpmidi_instance_data* data = calloc(1, sizeof(rtpmidi_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->fd = -1; data->control_fd = -1; inst->impl = data; - return inst; + return 0; } static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ @@ -910,7 +903,7 @@ static int rtpmidi_service(){ memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); - sendto(data->control_fd, &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + sendto(data->control_fd, (char*) &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); } else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){ //try to invite pre-defined unconnected applemidi peers @@ -922,7 +915,7 @@ static int rtpmidi_service(){ //append session name to packet memcpy(frame + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); - sendto(data->control_fd, invite, sizeof(apple_command) + strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + sendto(data->control_fd, (char*) invite, sizeof(apple_command) + strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); } } } @@ -947,10 +940,6 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ cfg.last_service = mm_timestamp(); } - if(!num){ - return 0; - } - for(u = 0; u < num; u++){ if(!fds[u].impl){ //TODO handle mDNS discovery input diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index db6237b..a08cf0f 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -6,7 +6,7 @@ MM_PLUGIN_API int init(); static int rtpmidi_configure(char* option, char* value); static int rtpmidi_configure_instance(instance* instance, char* option, char* value); -static instance* rtpmidi_instance(); +static int rtpmidi_instance(instance* inst); static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int rtpmidi_handle(size_t num, managed_fd* fds); -- cgit v1.2.3 From 18db3856a3a3e81f3e2050e3f137e6e15103f9a4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 24 Mar 2020 23:04:12 +0100 Subject: Simplify backend code according to new guarantees --- backends/artnet.c | 5 ----- backends/evdev.c | 8 -------- backends/jack.c | 52 +++++++++++++++++++++++++--------------------------- backends/lua.c | 3 ++- backends/osc.c | 14 +++++--------- backends/sacn.c | 9 --------- backends/winmidi.c | 5 ----- 7 files changed, 32 insertions(+), 64 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index caab6e0..c59a79d 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -378,11 +378,6 @@ static int artnet_handle(size_t num, managed_fd* fds){ } } - if(!num){ - //early exit - return 0; - } - for(u = 0; u < num; u++){ do{ bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); diff --git a/backends/evdev.c b/backends/evdev.c index af5ec74..8a14200 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -357,10 +357,6 @@ static int evdev_handle(size_t num, managed_fd* fds){ int read_status; struct input_event ev; - if(!num){ - return 0; - } - for(fd = 0; fd < num; fd++){ inst = (instance*) fds[fd].impl; if(!inst){ @@ -437,10 +433,6 @@ static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) int32_t value = 0; uint64_t range = 0; - if(!num){ - return 0; - } - if(!data->output_enabled){ LOGPF("Instance %s not enabled for output (%" PRIsize_t " channel events)", inst->name, num); return 0; diff --git a/backends/jack.c b/backends/jack.c index c862096..d430c60 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -549,38 +549,36 @@ static int mmjack_handle(size_t num, managed_fd* fds){ ssize_t bytes; uint8_t recv_buf[1024]; - if(num){ - for(u = 0; u < num; u++){ - inst = (instance*) fds[u].impl; - data = (mmjack_instance_data*) inst->impl; - bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); - if(bytes < 0){ - LOGPF("Failed to receive on feedback socket for instance %s", inst->name); - return 1; - } + for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + data = (mmjack_instance_data*) inst->impl; + bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); + if(bytes < 0){ + LOGPF("Failed to receive on feedback socket for instance %s", inst->name); + return 1; + } - for(p = 0; p < data->ports; p++){ - if(data->port[p].input && data->port[p].mark){ - pthread_mutex_lock(&data->port[p].lock); - switch(data->port[p].type){ - case port_cv: - mmjack_handle_cv(inst, p, data->port + p); - break; - case port_midi: - mmjack_handle_midi(inst, p, data->port + p); - break; - default: - LOGPF("Output handler not implemented for unknown channel type on %s.%s", inst->name, data->port[p].name); - break; - } - - data->port[p].mark = 0; - pthread_mutex_unlock(&data->port[p].lock); + for(p = 0; p < data->ports; p++){ + if(data->port[p].input && data->port[p].mark){ + pthread_mutex_lock(&data->port[p].lock); + switch(data->port[p].type){ + case port_cv: + mmjack_handle_cv(inst, p, data->port + p); + break; + case port_midi: + mmjack_handle_midi(inst, p, data->port + p); + break; + default: + LOGPF("Output handler not implemented for unknown channel type on %s.%s", inst->name, data->port[p].name); + break; } + + data->port[p].mark = 0; + pthread_mutex_unlock(&data->port[p].lock); } } } - + if(config.jack_shutdown){ LOG("Server disconnected"); return 1; diff --git a/backends/lua.c b/backends/lua.c index 7f80cc7..968193e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -471,7 +471,8 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ return NULL; } - data->channel[u].in = data->channel[u].out = 0.0; + //initialize new channel + memset(data->channel + u, 0, sizeof(lua_channel_data)); data->channel[u].name = strdup(spec); if(!data->channel[u].name){ LOG("Failed to allocate memory"); diff --git a/backends/osc.c b/backends/osc.c index 754c290..f40f3f5 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -231,7 +231,7 @@ static int osc_path_validate(char* path, uint8_t allow_patterns){ char pattern_chars[] = "?[]{}*"; size_t u, c; uint8_t square_open = 0, curly_open = 0; - + if(path[0] != '/'){ LOGPF("%s is not a valid OSC path: Missing root /", path); return 1; @@ -331,7 +331,7 @@ static int osc_path_match(char* pattern, char* path){ } if(pattern[match_end + 1] == '-' && pattern[match_end + 2] != ']'){ - if((pattern[match_end] > pattern[match_end + 2] + if((pattern[match_end] > pattern[match_end + 2] && path[u] >= pattern[match_end + 2] && path[u] <= pattern[match_end]) || (pattern[match_end] <= pattern[match_end + 2] @@ -666,7 +666,7 @@ static int osc_output_channel(instance* inst, size_t channel){ memcpy(xmit_buf, data->root, strlen(data->root)); offset += strlen(data->root); } - + memcpy(xmit_buf + offset, data->channel[channel].path, strlen(data->channel[channel].path)); offset += strlen(data->channel[channel].path) + 1; offset = osc_align(offset); @@ -703,16 +703,12 @@ static int osc_output_channel(instance* inst, size_t channel){ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t evt = 0, mark = 0; int rv = 0; + osc_instance_data* data = (osc_instance_data*) inst->impl; osc_channel_ident ident = { .label = 0 }; osc_parameter_value current; - if(!num){ - return 0; - } - - osc_instance_data* data = (osc_instance_data*) inst->impl; if(!data->dest_len){ LOGPF("Instance %s does not have a destination, output is disabled (%" PRIsize_t " channels)", inst->name, num); return 0; @@ -747,7 +743,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ mark = 1; } } - + if(mark){ //output all marked channels for(evt = 0; !rv && evt < num; evt++){ diff --git a/backends/sacn.c b/backends/sacn.c index bd5c75a..bce3887 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -330,10 +330,6 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ uint32_t frame_delta = 0; sacn_instance_data* data = (sacn_instance_data*) inst->impl; - if(!num){ - return 0; - } - if(!data->xmit_prio){ LOGPF("Instance %s not enabled for output (%" PRIsize_t " channel events)", inst->name, num); return 0; @@ -554,11 +550,6 @@ static int sacn_handle(size_t num, managed_fd* fds){ } } - //early exit - if(!num){ - return 0; - } - for(u = 0; u < num; u++){ do{ bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); diff --git a/backends/winmidi.c b/backends/winmidi.c index d9b3047..753f25c 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -185,11 +185,6 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v }; size_t u; - //early exit - if(!num){ - return 0; - } - if(!data->device_out){ LOGPF("Instance %s has no output device", inst->name); return 0; -- cgit v1.2.3 From 2a079f72483aa853d68430883b2281f436512c6b Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 26 Mar 2020 22:42:38 +0100 Subject: Implement lua cleanup handlers --- backends/lua.c | 74 +++++++++++++++++++++++++++++++++++++-------------------- backends/lua.h | 1 + backends/lua.md | 14 ++++++++--- 3 files changed, 60 insertions(+), 29 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 968193e..7424f65 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -155,8 +155,19 @@ static void lua_thread_resume(size_t current_thread){ lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); } -static int lua_callback_thread(lua_State* interpreter){ +static instance* lua_fetch_instance(lua_State* interpreter){ instance* inst = NULL; + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + lua_pop(interpreter, 1); + return inst; +} + +static int lua_callback_thread(lua_State* interpreter){ + instance* inst = lua_fetch_instance(interpreter); size_t u = threads; if(lua_gettop(interpreter) != 1){ LOGPF("Thread function called with %d arguments, expected function", lua_gettop(interpreter)); @@ -165,11 +176,6 @@ static int lua_callback_thread(lua_State* interpreter){ luaL_checktype(interpreter, 1, LUA_TFUNCTION); - //get instance pointer from registry - lua_pushstring(interpreter, LUA_REGISTRY_KEY); - lua_gettable(interpreter, LUA_REGISTRYINDEX); - inst = (instance*) lua_touserdata(interpreter, -1); - //make space for a new thread thread = realloc(thread, (threads + 1) * sizeof(lua_thread)); if(!thread){ @@ -223,20 +229,14 @@ static int lua_callback_output(lua_State* interpreter){ size_t n = 0; channel_value val; const char* channel_name = NULL; - instance* inst = NULL; - lua_instance_data* data = NULL; + instance* inst = lua_fetch_instance(interpreter); + lua_instance_data* data = (lua_instance_data*) inst->impl; if(lua_gettop(interpreter) != 2){ LOGPF("Output function called with %d arguments, expected 2 (string, number)", lua_gettop(interpreter)); return 0; } - //get instance pointer from registry - lua_pushstring(interpreter, LUA_REGISTRY_KEY); - lua_gettable(interpreter, LUA_REGISTRYINDEX); - inst = (instance*) lua_touserdata(interpreter, -1); - data = (lua_instance_data*) inst->impl; - //fetch function parameters channel_name = lua_tostring(interpreter, 1); val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); @@ -264,6 +264,28 @@ static int lua_callback_output(lua_State* interpreter){ return 0; } +static int lua_callback_cleanup_handler(lua_State* interpreter){ + instance* inst = lua_fetch_instance(interpreter); + lua_instance_data* data = (lua_instance_data*) inst->impl; + int current_handler = data->cleanup_handler; + + if(lua_gettop(interpreter) != 1){ + LOGPF("Cleanup handler function called with %d arguments, expected 1 (function)", lua_gettop(interpreter)); + return 0; + } + + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + + data->cleanup_handler = luaL_ref(interpreter, LUA_REGISTRYINDEX); + if(current_handler == LUA_NOREF){ + lua_pushnil(interpreter); + return 1; + } + lua_rawgeti(interpreter, LUA_REGISTRYINDEX, current_handler); + luaL_unref(interpreter, LUA_REGISTRYINDEX, current_handler); + return 1; +} + static int lua_callback_interval(lua_State* interpreter){ size_t n = 0; uint64_t interval = 0; @@ -274,10 +296,6 @@ static int lua_callback_interval(lua_State* interpreter){ return 0; } - //get instance pointer from registry - lua_pushstring(interpreter, LUA_REGISTRY_KEY); - lua_gettable(interpreter, LUA_REGISTRYINDEX); - //fetch and round the interval interval = luaL_checkinteger(interpreter, 2); if(interval % 10 < 5){ @@ -341,21 +359,15 @@ static int lua_callback_interval(lua_State* interpreter){ static int lua_callback_value(lua_State* interpreter, uint8_t input){ size_t n = 0; - instance* inst = NULL; - lua_instance_data* data = NULL; const char* channel_name = NULL; + instance* inst = lua_fetch_instance(interpreter); + lua_instance_data* data = (lua_instance_data*) inst->impl; if(lua_gettop(interpreter) != 1){ LOGPF("get_value function called with %d arguments, expected 1 (string)", lua_gettop(interpreter)); return 0; } - //get instance pointer from registry - lua_pushstring(interpreter, LUA_REGISTRY_KEY); - lua_gettable(interpreter, LUA_REGISTRYINDEX); - inst = (instance*) lua_touserdata(interpreter, -1); - data = (lua_instance_data*) inst->impl; - //fetch argument channel_name = lua_tostring(interpreter, 1); @@ -425,6 +437,7 @@ static int lua_instance(instance* inst){ //load the interpreter data->interpreter = luaL_newstate(); + data->cleanup_handler = LUA_NOREF; if(!data->interpreter){ LOG("Failed to initialize interpreter"); free(data); @@ -441,6 +454,7 @@ static int lua_instance(instance* inst){ lua_register(data->interpreter, "timestamp", lua_callback_timestamp); 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); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -578,6 +592,7 @@ static int lua_resolve_symbol(lua_State* interpreter, char* symbol){ || !strcmp(symbol, "output_value") || !strcmp(symbol, "input_channel") || !strcmp(symbol, "timestamp") + || !strcmp(symbol, "cleanup_handler") || !strcmp(symbol, "interval")){ return LUA_NOREF; } @@ -639,6 +654,13 @@ static int lua_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (lua_instance_data*) inst[u]->impl; + + //call cleanup function if one is registered + if(data->cleanup_handler != LUA_NOREF){ + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->cleanup_handler); + lua_pcall(data->interpreter, 0, 0, 0); + } + //stop the interpreter lua_close(data->interpreter); //cleanup channel data diff --git a/backends/lua.h b/backends/lua.h index 4583dfe..5587bf9 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -35,6 +35,7 @@ typedef struct /*_lua_instance_data*/ { lua_channel_data* channel; lua_State* interpreter; + int cleanup_handler; char* default_handler; } lua_instance_data; diff --git a/backends/lua.md b/backends/lua.md index 05509b6..30d7580 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -13,14 +13,15 @@ The following functions are provided within the Lua interpreter for interaction | Function | Usage example | Description | |-------------------------------|-------------------------------|---------------------------------------| -| `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel | +| `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel on this instance | | `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function | -| `input_value(string)` | `input_value("foo")` | Get the last input value on a channel | -| `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | +| `input_value(string)` | `input_value("foo")` | Get the last input value on a channel on this instance | +| `output_value(string)` | `output_value("bar")` | Get the last output value on a channel on this instance | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | | `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 | +| `cleanup_handler(function)` | | 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. | Example script: ```lua @@ -43,8 +44,12 @@ function run_show() end end +function save_values() +end + interval(toggle, 1000) thread(run_show) +cleanup_handler(save_values) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. @@ -86,6 +91,9 @@ be called. Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. This is intentional. +Output events generated from cleanup handlers called during shutdown will not be routed, as the core +routing facility has already shut down at this point. There are no plans to change this behaviour. + To build (and run) the `lua` backend on Windows, a compiled version of the Lua 5.3 library is required. For various reasons (legal, separations of concern, not wanting to ship binary data in the repository), the MIDIMonster project can not provide this file within this repository. -- cgit v1.2.3 From 253125ea28925e5207c375987ac36468327bed66 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 27 Mar 2020 21:40:45 +0100 Subject: Implement python cleanup handlers --- backends/lua.c | 9 +++-- backends/lua.md | 3 +- backends/python.c | 99 ++++++++++++++++++++++++++++++++++++------------------ backends/python.h | 1 + backends/python.md | 16 ++++++--- 5 files changed, 87 insertions(+), 41 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 7424f65..127933a 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -274,10 +274,13 @@ static int lua_callback_cleanup_handler(lua_State* interpreter){ return 0; } - luaL_checktype(interpreter, 1, LUA_TFUNCTION); + if(lua_type(interpreter, 1) != LUA_TFUNCTION && lua_type(interpreter, 1) != LUA_TNIL){ + LOG("Cleanup handler function parameter was neither nil nor a function"); + return 0; + } data->cleanup_handler = luaL_ref(interpreter, LUA_REGISTRYINDEX); - if(current_handler == LUA_NOREF){ + if(current_handler == LUA_NOREF || current_handler == LUA_REFNIL){ lua_pushnil(interpreter); return 1; } @@ -656,7 +659,7 @@ static int lua_shutdown(size_t n, instance** inst){ data = (lua_instance_data*) inst[u]->impl; //call cleanup function if one is registered - if(data->cleanup_handler != LUA_NOREF){ + if(data->cleanup_handler != LUA_NOREF && data->cleanup_handler != LUA_REFNIL){ lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->cleanup_handler); lua_pcall(data->interpreter, 0, 0, 0); } diff --git a/backends/lua.md b/backends/lua.md index 30d7580..e59e513 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -15,13 +15,13 @@ The following functions are provided within the Lua interpreter for interaction |-------------------------------|-------------------------------|---------------------------------------| | `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel on this instance | | `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function | +| `cleanup_handler(function)` | `cleanup_handler(shutdown)` | 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. | | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel on this instance | | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel on this instance | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | | `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 | -| `cleanup_handler(function)` | | 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. | Example script: ```lua @@ -45,6 +45,7 @@ function run_show() end function save_values() + -- Store state to a file, for example end interval(toggle, 1000) diff --git a/backends/python.c b/backends/python.c index 9f1d642..4c9248d 100644 --- a/backends/python.c +++ b/backends/python.c @@ -257,6 +257,33 @@ static PyObject* mmpy_interval(PyObject* self, PyObject* args){ return Py_None; } +static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + PyObject* current_handler = data->cleanup_handler; + + if(!PyArg_ParseTuple(args, "O", &(data->cleanup_handler)) + || (data->cleanup_handler != Py_None && !PyCallable_Check(data->cleanup_handler))){ + data->cleanup_handler = current_handler; + return NULL; + } + + if(data->cleanup_handler == Py_None){ + data->cleanup_handler = NULL; + } + else{ + Py_INCREF(data->cleanup_handler); + } + + if(!current_handler){ + Py_INCREF(Py_None); + return Py_None; + } + + Py_DECREF(current_handler); + return current_handler; +} + static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){ instance* inst = *((instance**) PyModule_GetState(self)); python_instance_data* data = (python_instance_data*) inst->impl; @@ -264,12 +291,10 @@ static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){ size_t u = 0, last_free = 0; int fd = -1; - if(!PyArg_ParseTuple(args, "OO", &handler, &sock)){ - return NULL; - } - - if(handler != Py_None && !PyCallable_Check(handler)){ - PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable"); + if(!PyArg_ParseTuple(args, "OO", &handler, &sock) + || sock == Py_None + || (handler != Py_None && !PyCallable_Check(handler))){ + PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable and a socket-like object"); return NULL; } @@ -398,13 +423,14 @@ static PyObject* mmpy_init(){ }; static PyMethodDef mmpy_methods[] = { - {"output", mmpy_output, METH_VARARGS, "Output a channel event"}, - {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel"}, - {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel"}, + {"output", mmpy_output, METH_VARARGS, "Output a channel event on the instance"}, + {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel on the instance"}, + {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel on the instance"}, {"current", mmpy_current_handler, METH_VARARGS, "Get the name of the currently executing channel handler"}, {"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"}, + {"cleanup_handler", mmpy_cleanup_handler, METH_VARARGS, "Register or update the instances cleanup handler"}, {0} }; @@ -674,29 +700,44 @@ static int python_start(size_t n, instance** inst){ static int python_shutdown(size_t n, instance** inst){ size_t u, p; + PyObject* result = NULL; python_instance_data* data = NULL; - //clean up channels - //this needs to be done before stopping the interpreters, - //because the handler references are refcounted - for(u = 0; u < n; u++){ - data = (python_instance_data*) inst[u]->impl; - for(p = 0; p < data->channels; p++){ - free(data->channel[p].name); - Py_XDECREF(data->channel[p].handler); + //if there are no instances, the python interpreter is not started, so cleanup can be skipped + if(python_main){ + //release interval references + for(p = 0; p < intervals; p++){ + //swap to interpreter + PyEval_RestoreThread(interval[p].interpreter); + Py_XDECREF(interval[p].reference); + PyEval_ReleaseThread(interval[p].interpreter); } - free(data->channel); - free(data->default_handler); - //do not free data here, needed for shutting down interpreters - } - if(python_main){ - //just used to lock the GIL + //lock the GIL for later interpreter release PyEval_RestoreThread(python_main); for(u = 0; u < n; u++){ data = (python_instance_data*) inst[u]->impl; + //swap to interpreter to be safe for releasing the references + PyThreadState_Swap(data->interpreter); + + //run cleanup handler before cleaning up channel data to allow reading channel data + if(data->cleanup_handler){ + result = PyObject_CallFunction(data->cleanup_handler, NULL); + Py_XDECREF(result); + Py_XDECREF(data->cleanup_handler); + } + + //clean up channels + for(p = 0; p < data->channels; p++){ + free(data->channel[p].name); + Py_XDECREF(data->channel[p].handler); + } + free(data->channel); + free(data->default_handler); + Py_XDECREF(data->handler); + //close sockets for(p = 0; p < data->sockets; p++){ close(data->socket[p].fd); //FIXME does python do this on its own? @@ -704,26 +745,18 @@ static int python_shutdown(size_t n, instance** inst){ Py_XDECREF(data->socket[p].handler); } - //release interval references - for(p = 0; p handler); - + //shut down interpreter, GIL is held after this but state is NULL DBGPF("Shutting down interpreter for instance %s", inst[u]->name); - //swap to interpreter and end it, GIL is held after this but state is NULL - PyThreadState_Swap(data->interpreter); PyErr_Clear(); //PyThreadState_Clear(data->interpreter); Py_EndInterpreter(data->interpreter); - free(data); } //shut down main interpreter PyThreadState_Swap(python_main); if(Py_FinalizeEx()){ - LOG("Failed to destroy python interpreters"); + LOG("Failed to shut down python library"); } PyMem_RawFree(program_name); } diff --git a/backends/python.h b/backends/python.h index 020aeac..539389b 100644 --- a/backends/python.h +++ b/backends/python.h @@ -45,4 +45,5 @@ typedef struct /*_python_instance_data*/ { char* default_handler; PyObject* handler; + PyObject* cleanup_handler; } python_instance_data; diff --git a/backends/python.md b/backends/python.md index 6852a79..ab0fb38 100644 --- a/backends/python.md +++ b/backends/python.md @@ -17,13 +17,14 @@ The `midimonster` module provides the following functions: | Function | Usage example | Description | |-------------------------------|---------------------------------------|-----------------------------------------------| -| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel | -| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel | -| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel | +| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel on this instance | +| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel of this instance | +| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel of this instance | | `current()` | `print(midimonster.current())` | Returns the name of the input channel whose handler function is currently running or `None` if the interpreter was called from another context | | `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 | +| `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 | +| `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. | Example Python module: ```python @@ -48,12 +49,16 @@ def socket_handler(sock): def ping(): print(midimonster.timestamp()) +def save_positions(): + # Store some data to disk + # Register an interval midimonster.interval(ping, 1000) # Create and register a client socket (add error handling as you like) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("localhost", 8990)) midimonster.manage(socket_handler, s) +midimonster.cleanup_handler(save_positions) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. @@ -92,6 +97,9 @@ py1.out1 > py2.module.handler Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. This is intentional. +Output events generated from cleanup handlers called during shutdown will not be routed, as the core +routing facility has already shut down at this point. There are no plans to change this behaviour. + Importing a Python module named `midimonster` is probably a bad idea and thus unsupported. The MIDIMonster is, at its core, single-threaded. Do not try to use Python's `threading` -- cgit v1.2.3 From 5fdb638454ce90eb565555dfece5030a2ec4a576 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 28 Mar 2020 01:09:58 +0100 Subject: Fix reference counting bug and lua timing --- backends/lua.c | 6 ++---- backends/python.c | 7 +++++-- 2 files changed, 7 insertions(+), 6 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index 127933a..e2f3b0e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -533,7 +533,8 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ } static int lua_handle(size_t num, managed_fd* fds){ - uint64_t delta = timer_interval; + uint64_t delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); size_t n; #ifdef MMBACKEND_LUA_TIMERFD @@ -547,9 +548,6 @@ static int lua_handle(size_t num, managed_fd* fds){ LOGPF("Failed to read timer: %s", strerror(errno)); return 1; } - #else - delta = mm_timestamp() - last_timestamp; - last_timestamp = mm_timestamp(); #endif //no timers active diff --git a/backends/python.c b/backends/python.c index 4c9248d..94f8e24 100644 --- a/backends/python.c +++ b/backends/python.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "python" +#define DEBUG #define PY_SSIZE_T_CLEAN #include @@ -269,9 +270,11 @@ static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){ } if(data->cleanup_handler == Py_None){ + DBGPF("Cleanup handler removed on %s (previously %s)", inst->name, current_handler ? "active" : "inactive"); data->cleanup_handler = NULL; } else{ + DBGPF("Cleanup handler installed on %s (previously %s)", inst->name, current_handler ? "active" : "inactive"); Py_INCREF(data->cleanup_handler); } @@ -280,7 +283,7 @@ static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){ return Py_None; } - Py_DECREF(current_handler); + //do not decrease refcount on current_handler here as the reference may be used by python code again return current_handler; } @@ -595,6 +598,7 @@ static int python_handle(size_t num, managed_fd* fds){ //if timer expired, call handler if(interval[u].delta >= interval[u].interval){ interval[u].delta %= interval[u].interval; + DBGPF("Calling interval handler %" PRIsize_t ", last delta %" PRIu64, u, delta); //swap to interpreter PyEval_RestoreThread(interval[u].interpreter); @@ -603,7 +607,6 @@ static int python_handle(size_t num, managed_fd* fds){ Py_XDECREF(result); //release interpreter PyEval_ReleaseThread(interval[u].interpreter); - DBGPF("Calling interval handler %" PRIsize_t, u); } } } -- cgit v1.2.3 From cf6a8e9a0f125633b1ef7d84e10935695598e654 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 29 Mar 2020 11:14:58 +0200 Subject: Reschedule artnet frame output on EAGAIN --- backends/artnet.c | 38 ++++++++++++++++++++++++++------------ backends/python.c | 1 - 2 files changed, 26 insertions(+), 13 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index c59a79d..585895b 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -206,10 +206,10 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ return data->data.channel + chan_a; } -static int artnet_transmit(instance* inst){ - size_t u; +static int artnet_transmit(instance* inst, artnet_output_universe* output){ artnet_instance_data* data = (artnet_instance_data*) inst->impl; - //output frame + + //build output frame artnet_pkt frame = { .magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00}, .opcode = htobe16(OpDmx), @@ -224,16 +224,30 @@ static int artnet_transmit(instance* inst){ 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){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #ifndef _WIN32 + if(errno != EAGAIN){ + LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #else + if(WSAGetLastError() != WSAEWOULDBLOCK){ + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + LOGPF("Failed to output frame for instance %s: %s", inst->name, error); + LocalFree(error); + #endif + return 1; + } + //reschedule frame output + output->mark = 1; + if(!next_frame || next_frame > ARTNET_SYNTHESIZE_MARGIN){ + next_frame = ARTNET_SYNTHESIZE_MARGIN; + } + return 0; } //update last frame timestamp - for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){ - if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){ - artnet_fd[data->fd_index].output_instance[u].last_frame = mm_timestamp(); - artnet_fd[data->fd_index].output_instance[u].mark = 0; - } - } + output->last_frame = mm_timestamp(); + output->mark = 0; return 0; } @@ -285,7 +299,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) } return 0; } - return artnet_transmit(inst); + return artnet_transmit(inst, artnet_fd[data->fd_index].output_instance + u); } return 0; @@ -366,7 +380,7 @@ static int artnet_handle(size_t num, managed_fd* fds){ || synthesize_delta >= ARTNET_KEEPALIVE_INTERVAL){ //keepalive timeout inst = mm_instance_find(BACKEND_NAME, artnet_fd[u].output_instance[c].label); if(inst){ - artnet_transmit(inst); + artnet_transmit(inst, artnet_fd[u].output_instance + c); } } diff --git a/backends/python.c b/backends/python.c index 94f8e24..28b95a9 100644 --- a/backends/python.c +++ b/backends/python.c @@ -1,5 +1,4 @@ #define BACKEND_NAME "python" -#define DEBUG #define PY_SSIZE_T_CLEAN #include -- cgit v1.2.3 From 3c6d2ccce4dd9be3f6497fb75d0456900e16ab0c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 29 Mar 2020 11:28:44 +0200 Subject: Reschedule sacn frame output on EAGAIN --- backends/sacn.c | 64 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 18 deletions(-) (limited to 'backends') diff --git a/backends/sacn.c b/backends/sacn.c index bce3887..b3376d7 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -275,9 +275,10 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ return data->data.channel + chan_a; } -static int sacn_transmit(instance* inst){ - size_t u; +static int sacn_transmit(instance* inst, sacn_output_universe* output){ sacn_instance_data* data = (sacn_instance_data*) inst->impl; + + //build sacn frame sacn_data_pdu pdu = { .root = { .preamble_size = htobe16(0x10), @@ -312,16 +313,31 @@ static int sacn_transmit(instance* inst){ memcpy((((uint8_t*)pdu.data.data) + 1), data->data.out, 512); if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); - } + #ifndef _WIN32 + if(errno != EAGAIN){ + LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #else + if(WSAGetLastError() != WSAEWOULDBLOCK){ + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + LOGPF("Failed to output frame for instance %s: %s", inst->name, error); + LocalFree(error); + #endif + return 1; + } - //update last transmit timestamp, unmark 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){ - global_cfg.fd[data->fd_index].universe[u].last_frame = mm_timestamp(); - global_cfg.fd[data->fd_index].universe[u].mark = 0; + //reschedule output + output->mark = 1; + if(!global_cfg.next_frame || global_cfg.next_frame > SACN_SYNTHESIZE_MARGIN){ + global_cfg.next_frame = SACN_SYNTHESIZE_MARGIN; } + return 0; } + + //update last transmit timestamp, unmark instance + output->last_frame = mm_timestamp(); + output->mark = 0; return 0; } @@ -357,14 +373,14 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ //send packet if required if(mark){ - if(!data->realtime){ - //find output instance data - 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; - } + //find output instance data + 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; } + } + if(!data->realtime){ frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].universe[u].last_frame; //check if ratelimiting engaged @@ -376,7 +392,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ return 0; } } - sacn_transmit(inst); + sacn_transmit(inst, global_cfg.fd[data->fd_index].universe + u); } return 0; @@ -496,7 +512,19 @@ static void sacn_discovery(size_t fd){ memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t)); if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ - LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, strerror(errno)); + #ifndef _WIN32 + if(errno != EAGAIN){ + LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, strerror(errno)); + } + #else + if(WSAGetLastError() != WSAEWOULDBLOCK){ + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, error); + LocalFree(error); + } + #endif } } } @@ -537,7 +565,7 @@ static int sacn_handle(size_t num, managed_fd* fds){ instance_id.fields.uni = global_cfg.fd[u].universe[c].universe; inst = mm_instance_find(BACKEND_NAME, instance_id.label); if(inst){ - sacn_transmit(inst); + sacn_transmit(inst, global_cfg.fd[u].universe + c); } } -- cgit v1.2.3 From 1891979d878e946941c6f236c3393cae943a4f1d Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 30 Mar 2020 03:15:36 +0200 Subject: Fix Coverity CID 355268 --- backends/lua.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index e2f3b0e..d7f2643 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -239,10 +239,15 @@ static int lua_callback_output(lua_State* interpreter){ //fetch function parameters channel_name = lua_tostring(interpreter, 1); + if(!channel_name){ + LOG("Output function called with invalid channel specification"); + return 0; + } + val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); //if not started yet, create any requested channels so scripts may set them at load time - if(!last_timestamp && channel_name){ + if(!last_timestamp){ lua_channel(inst, (char*) channel_name, mmchannel_output); } @@ -373,6 +378,10 @@ static int lua_callback_value(lua_State* interpreter, uint8_t input){ //fetch argument channel_name = lua_tostring(interpreter, 1); + if(!channel_name){ + LOG("get_value function called with invalid channel specification"); + return 0; + } //find correct channel & return value for(n = 0; n < data->channels; n++){ -- cgit v1.2.3 From bc6b25e14f3ddd8a405a974a4a2e03b9a71d4c9d Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 2 Apr 2020 23:52:28 +0200 Subject: Implement basic DNS parsing --- backends/rtpmidi.c | 246 +++++++++++++++++++++++++++++++++++++++++++--------- backends/rtpmidi.h | 33 +++++++ backends/rtpmidi.md | 4 +- 3 files changed, 237 insertions(+), 46 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 2eec871..97364cf 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -11,9 +11,9 @@ #include "rtpmidi.h" //TODO learn peer ssrcs -//TODO default session join? //TODO default mode? //TODO internal loop mode +//FIXME what qualifies an instance to be announced via mdns? static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -41,6 +41,7 @@ MM_PLUGIN_API int init(){ .conf_instance = rtpmidi_configure_instance, .channel = rtpmidi_channel, .handle = rtpmidi_set, + .interval = rtpmidi_interval, .process = rtpmidi_handle, .start = rtpmidi_start, .shutdown = rtpmidi_shutdown @@ -59,38 +60,90 @@ MM_PLUGIN_API int init(){ return 0; } -static int rtpmidi_configure(char* option, char* value){ - char* host = NULL, *port = NULL; +static int dns_decode_name(uint8_t* buffer, size_t len, size_t start, dns_name* out){ + size_t offset = 0, output_offset = 0; + uint8_t current_label = 0; + uint16_t ptr_target = 0; - if(!strcmp(option, "mdns-name")){ - if(cfg.mdns_name){ - LOG("Duplicate mdns-name assignment"); - return 1; + //reset output data length and terminate null name + out->length = 0; + if(out->name){ + out->name[0] = 0; + } + + while(start + offset < len){ + current_label = buffer[start + offset]; + + //if we're at a pointer, move there and stop counting data length + if(DNS_POINTER(current_label)){ + if(start + offset + 1 >= len){ + LOG("mDNS internal pointer out of bounds"); + return 1; + } + + //do this before setting the target + if(!ptr_target){ + out->length += 2; + } + + //calculate pointer target + ptr_target = DNS_LABEL_LENGTH(current_label) << 8 | buffer[start + offset + 1]; + + if(ptr_target >= len){ + LOG("mDNS internal pointer target out of bounds"); + return 1; + } + start = ptr_target; + offset = 0; } + else{ + if(DNS_LABEL_LENGTH(current_label) == 0){ + if(!ptr_target){ + out->length++; + } + break; + } - cfg.mdns_name = strdup(value); - if(!cfg.mdns_name){ - LOG("Failed to allocate memory"); - return 1; + //check whether we have the bytes we need + if(start + offset + DNS_LABEL_LENGTH(current_label) > len){ + LOG("mDNS bytes missing"); + return 1; + } + + //check whether we have space in the output + if(output_offset + DNS_LABEL_LENGTH(current_label) > out->alloc){ + out->name = realloc(out->name, (output_offset + DNS_LABEL_LENGTH(current_label) + 2) * sizeof(uint8_t)); + out->alloc = output_offset + DNS_LABEL_LENGTH(current_label); + } + + //copy data from this label to output buffer + memcpy(out->name + output_offset, buffer + start + offset + 1, DNS_LABEL_LENGTH(current_label)); + output_offset += DNS_LABEL_LENGTH(current_label) + 1; + offset += DNS_LABEL_LENGTH(current_label) + 1; + out->name[output_offset - 1] = '.'; + out->name[output_offset] = 0; + if(!ptr_target){ + out->length = offset; + } } - return 0; } - else if(!strcmp(option, "mdns-bind")){ - if(cfg.mdns_fd >= 0){ - LOG( "Only one mDNS discovery bind is supported"); - return 1; - } + return 0; +} - mmbackend_parse_hostspec(value, &host, &port, NULL); +static uint32_t rtpmidi_interval(){ + return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); +} - if(!host){ - LOGPF("Not a valid mDNS bind address: %s", value); +static int rtpmidi_configure(char* option, char* value){ + if(!strcmp(option, "mdns-name")){ + if(cfg.mdns_name){ + LOG("Duplicate mdns-name assignment"); return 1; } - cfg.mdns_fd = mmbackend_socket(host, (port ? port : RTPMIDI_MDNS_PORT), SOCK_DGRAM, 1, 1); - if(cfg.mdns_fd < 0){ - LOGPF("Failed to bind mDNS interface: %s", value); + cfg.mdns_name = strdup(value); + if(!cfg.mdns_name){ + LOG("Failed to allocate memory"); return 1; } return 0; @@ -925,6 +978,79 @@ static int rtpmidi_service(){ return 0; } +static int rtpmidi_handle_mdns(){ + uint8_t buffer[RTPMIDI_PACKET_BUFFER]; + dns_header* hdr = (dns_header*) buffer; + dns_rr* rr = NULL; + dns_name name = { + .alloc = 0 + }; + ssize_t bytes = 0; + size_t u = 0, offset = sizeof(dns_header); + + for(bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0); bytes > 0; bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0)){ + if(bytes < sizeof(dns_header)){ + continue; + } + LOGPF("%" PRIsize_t " bytes of mDNS data", bytes); + + hdr->id = be16toh(hdr->id); + hdr->questions = be16toh(hdr->questions); + hdr->answers = be16toh(hdr->answers); + hdr->servers = be16toh(hdr->servers); + hdr->additional = be16toh(hdr->additional); + + //TODO Check for ._apple-midi._udp.local. + LOGPF("ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); + for(u = 0; u < hdr->questions; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + offset += sizeof(dns_question); + + LOGPF("Question: %s", name.name); + } + + for(u = 0; u < hdr->answers; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr) + be16toh(rr->data); + + LOGPF("Answer (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + } + + for(u = 0; u < hdr->servers; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr) + be16toh(rr->data); + + LOGPF("Authority (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + } + + for(u = 0; u < hdr->additional; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr) + be16toh(rr->data); + + LOGPF("Additional (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + } + } + + free(name.name); + if(bytes <= 0){ + if(errno == EAGAIN){ + return 0; + } + + LOGPF("Error reading from mDNS descriptor: %s", strerror(errno)); + return 1; + } + + return 0; +} + static int rtpmidi_handle(size_t num, managed_fd* fds){ size_t u; int rv = 0; @@ -933,7 +1059,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ //handle service tasks (mdns, clock sync, peer connections) if(mm_timestamp() - cfg.last_service > RTPMIDI_SERVICE_INTERVAL){ - DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service); + //DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service); if(rtpmidi_service()){ return 1; } @@ -942,7 +1068,8 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ for(u = 0; u < num; u++){ if(!fds[u].impl){ - //TODO handle mDNS discovery input + //handle mDNS discovery input + rtpmidi_handle_mdns(); } else{ //handle rtp/control input @@ -963,30 +1090,53 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ return rv; } -static int rtpmidi_start(size_t n, instance** inst){ - size_t u, p, fds = 0; - rtpmidi_instance_data* data = NULL; +static int rtpmidi_start_mdns(){ + struct ip_mreq mcast_req = { + .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xe00000fb)), + .imr_interface.s_addr = INADDR_ANY + }; - //if mdns name defined and no socket, bind default values - if(cfg.mdns_name && cfg.mdns_fd < 0){ - cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); - if(cfg.mdns_fd < 0){ - return 1; - } + struct ipv6_mreq mcast6_req = { + .ipv6mr_multiaddr.s6_addr = {0xff, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xfb}, + .ipv6mr_interface = 0 + }; + + if(!cfg.mdns_name){ + LOG("No mDNS name set, disabling AppleMIDI discovery"); + return 0; } - //register mdns fd to core - if(cfg.mdns_fd >= 0){ - if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ - LOG("Failed to register mDNS socket with core"); - return 1; - } - fds++; + //FIXME might try passing NULL as host here to work around possible windows ipv6 handicaps + cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); + if(cfg.mdns_fd < 0){ + LOG("Failed to create requested mDNS descriptor"); + return 1; } - else{ - LOG("No mDNS discovery interface bound, AppleMIDI session discovery disabled"); + + //join ipv4 multicast group + if(setsockopt(cfg.mdns_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ + LOGPF("Failed to join IPv4 multicast group for mDNS: %s", strerror(errno)); + return 1; + } + + //join ipv6 multicast group + if(setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ + LOGPF("Failed to join IPv6 multicast group for mDNS: %s", strerror(errno)); + return 1; } + //register mdns fd to core + return mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL); +} + +static int rtpmidi_start(size_t n, instance** inst){ + size_t u, p, fds = 0; + rtpmidi_instance_data* data = NULL; + uint8_t mdns_required = 0; + for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; //check whether instances are explicitly configured to a mode @@ -1013,6 +1163,9 @@ static int rtpmidi_start(size_t n, instance** inst){ data->peer[p].connected = 1; } } + else if(data->mode == apple){ + mdns_required = 1; + } //register fds to core if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){ @@ -1022,6 +1175,13 @@ static int rtpmidi_start(size_t n, instance** inst){ fds += (data->control_fd >= 0) ? 2 : 1; } + if(mdns_required && rtpmidi_start_mdns()){ + return 1; + } + else{ + fds++; + } + LOGPF("Registered %" PRIsize_t " descriptors to core", fds); return 0; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index a08cf0f..b1fe26a 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -8,6 +8,7 @@ static int rtpmidi_configure(char* option, char* value); static int rtpmidi_configure_instance(instance* instance, char* option, char* value); static int rtpmidi_instance(instance* inst); static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); +static uint32_t rtpmidi_interval(); static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int rtpmidi_handle(size_t num, managed_fd* fds); static int rtpmidi_start(size_t n, instance** inst); @@ -22,6 +23,11 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_DEFAULT_NAME "MIDIMonster" #define RTPMIDI_SERVICE_INTERVAL 1000 +#define DNS_POINTER(a) (((a) & 0xC0) == 0xC0) +#define DNS_LABEL_LENGTH(a) ((a) & 0x3F) +#define DNS_OPCODE(a) (((a) & 0x78) >> 3) +#define DNS_RESPONSE(a) ((a) & 0x80) + enum /*_rtpmidi_channel_type*/ { none = 0, note = 0x90, @@ -90,6 +96,12 @@ enum applemidi_command { apple_feedback = 0x5253 //RS }; +typedef struct /*_dns_name*/ { + size_t alloc; + char* name; + size_t length; +} dns_name; + #pragma pack(push, 1) typedef struct /*_apple_session_command*/ { uint16_t res1; @@ -128,4 +140,25 @@ typedef struct /*_rtp_midi_command*/ { uint8_t flags; uint8_t length; } rtpmidi_command_header; + +typedef struct /*_dns_header*/ { + uint16_t id; + uint8_t flags[2]; + uint16_t questions; + uint16_t answers; + uint16_t servers; + uint16_t additional; +} dns_header; + +typedef struct /*_dns_question*/ { + uint16_t qtype; + uint16_t qclass; +} dns_question; + +typedef struct /*_dns_rr*/ { + uint16_t rtype; + uint16_t rclass; + uint32_t ttl; + uint16_t data; +} dns_rr; #pragma pack(pop) diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index e857a5a..c188039 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -25,7 +25,6 @@ stream, which may lead to inconsistencies during playback. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| | `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration | -| `mdns-bind` | `10.1.2.1 5353` | `:: 5353` | Bind host for the mDNS discovery server | | `mdns-name` | `computer1` | none | mDNS hostname to announce, also used as AppleMIDI peer name | #### Instance configuration @@ -51,9 +50,8 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | -| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times | +| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times. | | `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). Setting this option makes the instance a session participant | -| `peer` | `10.1.2.3 9001` | none | Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter (and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. -- cgit v1.2.3 From 789b3b31230750b50ad815add0a42a15760e8b83 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 4 Apr 2020 20:41:04 +0200 Subject: Basic mDNS discovery --- backends/rtpmidi.c | 228 +++++++++++++++++++++++++++++++++++++++++++++++++---- backends/rtpmidi.h | 9 +++ 2 files changed, 220 insertions(+), 17 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 97364cf..9e0a4d4 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -10,6 +10,8 @@ #include "libmmbackend.h" #include "rtpmidi.h" +//#include "../tests/hexdump.c" + //TODO learn peer ssrcs //TODO default mode? //TODO internal loop mode @@ -113,6 +115,10 @@ static int dns_decode_name(uint8_t* buffer, size_t len, size_t start, dns_name* //check whether we have space in the output if(output_offset + DNS_LABEL_LENGTH(current_label) > out->alloc){ out->name = realloc(out->name, (output_offset + DNS_LABEL_LENGTH(current_label) + 2) * sizeof(uint8_t)); + if(!out->name){ + LOG("Failed to allocate memory"); + return 1; + } out->alloc = output_offset + DNS_LABEL_LENGTH(current_label); } @@ -130,6 +136,43 @@ static int dns_decode_name(uint8_t* buffer, size_t len, size_t start, dns_name* return 0; } +static int dns_encode_name(char* name, dns_name* out){ + char* save = NULL, *token = NULL; + out->length = 0; + + for(token = strtok_r(name, ".", &save); token; token = strtok_r(NULL, ".", &save)){ + //make space for this label, its length and a trailing root label + if(out->alloc < out->length + strlen(token) + 1 + 1){ + out->name = realloc(out->name, (out->length + strlen(token) + 2) * sizeof(char)); + if(!out->name){ + LOG("Failed to allocate memory"); + return 1; + } + out->alloc = out->length + strlen(token) + 2; + } + //FIXME check label length before adding + out->name[out->length] = strlen(token); + memcpy(out->name + out->length + 1, token, strlen(token)); + out->length += strlen(token) + 1; + } + + //last-effort allocate a root buffer + if(!out->alloc){ + out->name = calloc(1, sizeof(char)); + if(!out->name){ + LOG("Failed to allocate memory"); + return 1; + } + out->alloc = 1; + } + + //add root label + out->name[out->length] = 0; + out->length++; + + return 0; +} + static uint32_t rtpmidi_interval(){ return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); } @@ -160,7 +203,7 @@ static int rtpmidi_configure(char* option, char* value){ return 1; } -static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* port){ +static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, char* host, char* port){ struct sockaddr_storage sock_addr = { 0 }; @@ -173,19 +216,26 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* return 1; } + if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ + LOGPF("Failed to fetch data port information: %s", strerror(errno)); + return 1; + } + //bind control port if(data->mode == apple){ - if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ - LOGPF("Failed to fetch data port information: %s", strerror(errno)); - return 1; - } - - snprintf(control_port, sizeof(control_port), "%d", be16toh(((struct sockaddr_in*)&sock_addr)->sin_port) - 1); + data->control_port = be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) - 1; + snprintf(control_port, sizeof(control_port), "%d", data->control_port); data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0); if(data->control_fd < 0){ - LOGPF("Failed to bind control port %s", control_port); + LOGPF("Failed to bind control port %s for instance %s", control_port, inst->name); return 1; } + + LOGPF("Apple mode instance %s listening on port %d", inst->name, data->control_port); + } + else{ + data->control_port = be16toh(((struct sockaddr_in*)&sock_addr)->sin_port); + LOGPF("Direct mode instance %s listening on port %d", inst->name, data->control_port); } return 0; @@ -334,7 +384,7 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 1; } - return rtpmidi_bind_instance(data, host, port); + return rtpmidi_bind_instance(inst, data, host, port); } else if(!strcmp(option, "learn")){ if(data->mode != direct){ @@ -643,6 +693,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size } else if(command->command == apple_reject){ //just ignore this for now and retry the invitation + LOGPF("Invitation rejected on instance %s", inst->name); } else if(command->command == apple_leave){ //remove peer from list - this comes in on the control port, but we need to remove the data port... @@ -910,6 +961,144 @@ static int rtpmidi_handle_control(instance* inst){ return 0; } +int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + dns_header* hdr = (dns_header*) frame; + dns_rr* rr = NULL; + dns_rr_srv* srv = NULL; + dns_name name = { + .alloc = 0 + }; + size_t offset = 0; + struct sockaddr_in mcast = { + .sin_family = AF_INET, + .sin_port = htobe16(5353), + .sin_addr.s_addr = htobe32(((uint32_t) 0xe00000fb)) + }; + struct sockaddr_in6 mcast6 = { + .sin6_family = AF_INET6, + .sin6_port = htobe16(5353), + .sin6_addr.s6_addr = {0xff, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xfb} + }; + + hdr->id = 0; + hdr->flags[0] = 0x84; + hdr->flags[1] = 0; + hdr->questions = hdr->servers = 0; + hdr->answers = htobe16(4); + hdr->additional = htobe16(1); + offset = sizeof(dns_header); + + //answer 1: SRV FQDN + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", data->session_name, RTPMIDI_MDNS_DOMAIN); + if(dns_encode_name((char*) frame + offset, &name)){ + LOGPF("Failed to encode name for %s", frame); + return 1; + } + + memcpy(frame + offset, name.name, name.length); + offset += name.length; + + rr = (dns_rr*) (frame + offset); + rr->rtype = htobe16(33); //SRV RR + rr->rclass = htobe16(1); //INADDR + rr->ttl = htobe32(120); + offset += sizeof(dns_rr); + + srv = (dns_rr_srv*) (frame + offset); + srv->priority = 0; + srv->weight = 0; + srv->port = htobe16(data->control_port); + offset += sizeof(dns_rr_srv); + + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); + if(dns_encode_name((char*) frame + offset, &name)){ + LOGPF("Failed to encode name for %s", frame + offset); + return 1; + } + memcpy(frame + offset, name.name, name.length); + offset += name.length; + rr->data = htobe16(sizeof(dns_rr_srv) + name.length); + + //answer 2: empty TXT (apple asks for it otherwise) + frame[offset++] = 0xC0; + frame[offset++] = sizeof(dns_header); + rr = (dns_rr*) (frame + offset); + rr->rtype = htobe16(16); //TXT RR + rr->rclass = htobe16(1); //INADDR + rr->ttl = htobe32(4500); + rr->data = htobe16(1); + offset += sizeof(dns_rr); + frame[offset++] = 0x00; //zero-length TXT + + //answer 3: dns-sd PTR _applemidi + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s", RTPMIDI_DNSSD_DOMAIN); + if(dns_encode_name((char*) frame + offset, &name)){ + LOGPF("Failed to encode name for %s", frame + offset); + return 1; + } + memcpy(frame + offset, name.name, name.length); + offset += name.length; + + rr = (dns_rr*) (frame + offset); + rr->rtype = htobe16(12); //PTR RR + rr->rclass = htobe16(1); //INADDR + rr->ttl = htobe32(4500); + rr->data = htobe16(2); + offset += sizeof(dns_rr); + + //add backref for PTR + frame[offset++] = 0xC0; + frame[offset++] = sizeof(dns_header) + frame[sizeof(dns_header)] + 1; + + //answer 4: _applemidi PTR FQDN + frame[offset++] = 0xC0; + frame[offset++] = sizeof(dns_header) + frame[sizeof(dns_header)] + 1; + + rr = (dns_rr*) (frame + offset); + rr->rtype = htobe16(12); //PTR RR + rr->rclass = htobe16(1); //INADDR + rr->ttl = htobe32(4500); + rr->data = htobe16(2); + offset += sizeof(dns_rr); + + //add backref for PTR + frame[offset++] = 0xC0; + frame[offset++] = sizeof(dns_header); + + //additional 1: A host + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); + if(dns_encode_name((char*) frame + offset, &name)){ + LOGPF("Failed to encode name for %s", frame + offset); + return 1; + } + memcpy(frame + offset, name.name, name.length); + offset += name.length; + + rr = (dns_rr*) (frame + offset); + rr->rtype = htobe16(1); //A RR + rr->rclass = htobe16(1); //INADDR + rr->ttl = htobe32(120); + rr->data = htobe16(4); + offset += sizeof(dns_rr); + + //TODO get local addresses here + frame[offset++] = 0x0A; + frame[offset++] = 0x17; + frame[offset++] = 0x01; + frame[offset++] = 0x07; + + //send to ipv4 and ipv6 mcasts + sendto(cfg.mdns_fd, frame, offset, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); + sendto(cfg.mdns_fd, frame, offset, 0, (struct sockaddr*) &mcast, sizeof(mcast)); + + free(name.name); + return 0; +} + static int rtpmidi_service(){ size_t n, u, p; instance** inst = NULL; @@ -938,15 +1127,16 @@ static int rtpmidi_service(){ return 1; } - //mdns discovery - if(cfg.mdns_fd >= 0){ - //TODO send applemidi discovery packets - } for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; if(data->mode == apple){ + //mdns discovery + if(cfg.mdns_fd >= 0){ + rtpmidi_mdns_announce(data); + } + for(p = 0; p < data->peers; p++){ if(data->peer[p].active && data->peer[p].connected){ //apple sync @@ -986,7 +1176,7 @@ static int rtpmidi_handle_mdns(){ .alloc = 0 }; ssize_t bytes = 0; - size_t u = 0, offset = sizeof(dns_header); + size_t u = 0, offset = 0; for(bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0); bytes > 0; bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0)){ if(bytes < sizeof(dns_header)){ @@ -1000,14 +1190,18 @@ static int rtpmidi_handle_mdns(){ hdr->servers = be16toh(hdr->servers); hdr->additional = be16toh(hdr->additional); + //rfc6762 18.3: opcode != 0 -> ignore + //rfc6762 18.11: response code != 0 -> ignore + //TODO Check for ._apple-midi._udp.local. + offset = sizeof(dns_header); LOGPF("ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); for(u = 0; u < hdr->questions; u++){ dns_decode_name(buffer, bytes, offset, &name); offset += name.length; offset += sizeof(dns_question); - LOGPF("Question: %s", name.name); + LOGPF("Question (%d bytes): %s", name.length, name.name); } for(u = 0; u < hdr->answers; u++){ @@ -1151,7 +1345,7 @@ static int rtpmidi_start(size_t n, instance** inst){ } //if not bound, bind to default - if(data->fd < 0 && rtpmidi_bind_instance(data, RTPMIDI_DEFAULT_HOST, NULL)){ + if(data->fd < 0 && rtpmidi_bind_instance(inst[u], data, RTPMIDI_DEFAULT_HOST, NULL)){ LOGPF("Failed to bind default sockets for instance %s", inst[u]->name); return 1; } @@ -1178,7 +1372,7 @@ static int rtpmidi_start(size_t n, instance** inst){ if(mdns_required && rtpmidi_start_mdns()){ return 1; } - else{ + else if(mdns_required){ fds++; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index b1fe26a..f55b543 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -22,6 +22,8 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) #define RTPMIDI_DEFAULT_NAME "MIDIMonster" #define RTPMIDI_SERVICE_INTERVAL 1000 +#define RTPMIDI_MDNS_DOMAIN "_apple-midi._udp.local" +#define RTPMIDI_DNSSD_DOMAIN "_services._dns-sd._udp.local" #define DNS_POINTER(a) (((a) & 0xC0) == 0xC0) #define DNS_LABEL_LENGTH(a) ((a) & 0x3F) @@ -67,6 +69,7 @@ typedef struct /*_rtmidi_instance_data*/ { int fd; int control_fd; + uint16_t control_port; /*convenience member set by rtpmidi_bind_instance*/ size_t peers; rtpmidi_peer* peer; @@ -161,4 +164,10 @@ typedef struct /*_dns_rr*/ { uint32_t ttl; uint16_t data; } dns_rr; + +typedef struct /*_dns_rr_srv*/ { + uint16_t priority; + uint16_t weight; + uint16_t port; +} dns_rr_srv; #pragma pack(pop) -- cgit v1.2.3 From f28497227eba6783d7e779a5ccaf2a12c1794a94 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 6 Apr 2020 23:00:48 +0200 Subject: Discover peers, detach from DNS-SD at shutdown --- backends/rtpmidi.c | 265 ++++++++++++++++++++++++++++++++++------------------ backends/rtpmidi.h | 8 +- backends/rtpmidi.md | 21 +++-- 3 files changed, 190 insertions(+), 104 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 9e0a4d4..7ad3eca 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -15,7 +15,6 @@ //TODO learn peer ssrcs //TODO default mode? //TODO internal loop mode -//FIXME what qualifies an instance to be announced via mdns? static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -173,6 +172,48 @@ static int dns_encode_name(char* name, dns_name* out){ return 0; } +static ssize_t dns_push_rr(uint8_t* buffer, size_t length, dns_rr** out, char* name, uint16_t type, uint16_t class, uint32_t ttl, uint16_t len){ + dns_rr* rr = NULL; + size_t offset = 0; + dns_name encode = { + .alloc = 0 + }; + + //if requested, encode name + if(name && dns_encode_name(name, &encode)){ + LOGPF("Failed to encode DNS name %s", name); + goto bail; + } + + if(encode.length + sizeof(dns_rr) > length){ + LOGPF("Failed to encode DNS name %s, insufficient space", name); + goto bail; + } + + if(name){ + //copy encoded name to buffer + memcpy(buffer, encode.name, encode.length); + offset += encode.length; + } + + rr = (dns_rr*) (buffer + offset); + rr->rtype = htobe16(type); + rr->rclass = htobe16(class); + rr->ttl = htobe32(ttl); + rr->data = htobe16(len); + offset += sizeof(dns_rr); + if(out){ + *out = rr; + } + + free(encode.name); + return offset; + +bail: + free(encode.name); + return -1; +} + static uint32_t rtpmidi_interval(){ return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); } @@ -421,14 +462,18 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0); } - else if(!strcmp(option, "session")){ + else if(!strcmp(option, "title")){ if(data->mode != apple){ - LOG("'session' option is only valid for apple mode instances"); + LOG("'title' option is only valid for apple mode instances"); return 1; } - free(data->session_name); - data->session_name = strdup(value); - if(!data->session_name){ + if(strchr(value, '.')){ + LOGPF("Invalid instance title %s on %s: titles may not contain periods", value, inst->name); + return 1; + } + free(data->title); + data->title = strdup(value); + if(!data->title){ LOG("Failed to allocate memory"); return 1; } @@ -684,10 +729,10 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size invite->version = htobe32(2); invite->token = command->token; invite->ssrc = htobe32(data->ssrc); - memcpy(response + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); + memcpy(response + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); //calculate data port ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); - sendto(data->fd, response, sizeof(apple_command) + strlen(data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); + sendto(data->fd, response, sizeof(apple_command) + strlen(data->title ? data->title : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); } return 0; } @@ -961,15 +1006,7 @@ static int rtpmidi_handle_control(instance* inst){ return 0; } -int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ - uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; - dns_header* hdr = (dns_header*) frame; - dns_rr* rr = NULL; - dns_rr_srv* srv = NULL; - dns_name name = { - .alloc = 0 - }; - size_t offset = 0; +static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){ struct sockaddr_in mcast = { .sin_family = AF_INET, .sin_port = htobe16(5353), @@ -984,6 +1021,63 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ 0x00, 0x00, 0x00, 0xfb} }; + //send to ipv4 and ipv6 mcasts + sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); + sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)); + return 0; +} + +static int rtpmidi_mdns_detach(rtpmidi_instance_data* data){ + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + dns_header* hdr = (dns_header*) frame; + dns_rr* rr = NULL; + dns_name name = { + .alloc = 0 + }; + size_t offset = 0; + ssize_t bytes = 0; + + hdr->id = 0; + hdr->flags[0] = 0x84; + hdr->flags[1] = 0; + hdr->questions = hdr->servers = hdr->additional = 0; + hdr->answers = htobe16(1); + offset = sizeof(dns_header); + + //answer 1: _apple-midi PTR FQDN + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s", RTPMIDI_MDNS_DOMAIN); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 12, 1, 0, 0); + if(bytes < 0){ + goto bail; + } + offset += bytes; + + //TODO length-checks here + frame[offset++] = strlen(data->title); + memcpy(frame + offset, data->title, strlen(data->title)); + offset += strlen(data->title); + frame[offset++] = 0xC0; + frame[offset++] = sizeof(dns_header); + rr->data = htobe16(1 + strlen(data->title) + 2); + + free(name.name); + return rtpmidi_mdns_broadcast(frame, offset); +bail: + free(name.name); + return 1; +} + +static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + dns_header* hdr = (dns_header*) frame; + dns_rr* rr = NULL; + dns_rr_srv* srv = NULL; + dns_name name = { + .alloc = 0 + }; + size_t offset = 0; + ssize_t bytes = 0; + hdr->id = 0; hdr->flags[0] = 0x84; hdr->flags[1] = 0; @@ -993,20 +1087,12 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ offset = sizeof(dns_header); //answer 1: SRV FQDN - snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", data->session_name, RTPMIDI_MDNS_DOMAIN); - if(dns_encode_name((char*) frame + offset, &name)){ - LOGPF("Failed to encode name for %s", frame); - return 1; + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", data->title, RTPMIDI_MDNS_DOMAIN); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 33, 1, 120, 0); + if(bytes < 0){ + goto bail; } - - memcpy(frame + offset, name.name, name.length); - offset += name.length; - - rr = (dns_rr*) (frame + offset); - rr->rtype = htobe16(33); //SRV RR - rr->rclass = htobe16(1); //INADDR - rr->ttl = htobe32(120); - offset += sizeof(dns_rr); + offset += bytes; srv = (dns_rr_srv*) (frame + offset); srv->priority = 0; @@ -1017,7 +1103,7 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); if(dns_encode_name((char*) frame + offset, &name)){ LOGPF("Failed to encode name for %s", frame + offset); - return 1; + goto bail; } memcpy(frame + offset, name.name, name.length); offset += name.length; @@ -1026,29 +1112,21 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ //answer 2: empty TXT (apple asks for it otherwise) frame[offset++] = 0xC0; frame[offset++] = sizeof(dns_header); - rr = (dns_rr*) (frame + offset); - rr->rtype = htobe16(16); //TXT RR - rr->rclass = htobe16(1); //INADDR - rr->ttl = htobe32(4500); - rr->data = htobe16(1); - offset += sizeof(dns_rr); + + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, NULL, 16, 1, 4500, 1); + if(bytes < 0){ + goto bail; + } + offset += bytes; frame[offset++] = 0x00; //zero-length TXT //answer 3: dns-sd PTR _applemidi snprintf((char*) frame + offset, sizeof(frame) - offset, "%s", RTPMIDI_DNSSD_DOMAIN); - if(dns_encode_name((char*) frame + offset, &name)){ - LOGPF("Failed to encode name for %s", frame + offset); - return 1; + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 12, 1, 4500, 2); + if(bytes < 0){ + goto bail; } - memcpy(frame + offset, name.name, name.length); - offset += name.length; - - rr = (dns_rr*) (frame + offset); - rr->rtype = htobe16(12); //PTR RR - rr->rclass = htobe16(1); //INADDR - rr->ttl = htobe32(4500); - rr->data = htobe16(2); - offset += sizeof(dns_rr); + offset += bytes; //add backref for PTR frame[offset++] = 0xC0; @@ -1058,12 +1136,11 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ frame[offset++] = 0xC0; frame[offset++] = sizeof(dns_header) + frame[sizeof(dns_header)] + 1; - rr = (dns_rr*) (frame + offset); - rr->rtype = htobe16(12); //PTR RR - rr->rclass = htobe16(1); //INADDR - rr->ttl = htobe32(4500); - rr->data = htobe16(2); - offset += sizeof(dns_rr); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, NULL, 12, 1, 4500, 2); + if(bytes < 0){ + goto bail; + } + offset += bytes; //add backref for PTR frame[offset++] = 0xC0; @@ -1071,19 +1148,11 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ //additional 1: A host snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); - if(dns_encode_name((char*) frame + offset, &name)){ - LOGPF("Failed to encode name for %s", frame + offset); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 1, 1, 120, 4); + if(bytes < 0){ return 1; } - memcpy(frame + offset, name.name, name.length); - offset += name.length; - - rr = (dns_rr*) (frame + offset); - rr->rtype = htobe16(1); //A RR - rr->rclass = htobe16(1); //INADDR - rr->ttl = htobe32(120); - rr->data = htobe16(4); - offset += sizeof(dns_rr); + offset += bytes; //TODO get local addresses here frame[offset++] = 0x0A; @@ -1091,12 +1160,11 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ frame[offset++] = 0x01; frame[offset++] = 0x07; - //send to ipv4 and ipv6 mcasts - sendto(cfg.mdns_fd, frame, offset, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); - sendto(cfg.mdns_fd, frame, offset, 0, (struct sockaddr*) &mcast, sizeof(mcast)); - free(name.name); - return 0; + return rtpmidi_mdns_broadcast(frame, offset); +bail: + free(name.name); + return 1; } static int rtpmidi_service(){ @@ -1127,13 +1195,13 @@ static int rtpmidi_service(){ return 1; } - for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; if(data->mode == apple){ //mdns discovery - if(cfg.mdns_fd >= 0){ + //TODO time-out this properly + if(cfg.mdns_fd >= 0 && data->title){ rtpmidi_mdns_announce(data); } @@ -1156,9 +1224,9 @@ static int rtpmidi_service(){ memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); //append session name to packet - memcpy(frame + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); + memcpy(frame + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); - sendto(data->control_fd, (char*) invite, sizeof(apple_command) + strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + sendto(data->control_fd, (char*) invite, sizeof(apple_command) + strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); } } } @@ -1182,7 +1250,6 @@ static int rtpmidi_handle_mdns(){ if(bytes < sizeof(dns_header)){ continue; } - LOGPF("%" PRIsize_t " bytes of mDNS data", bytes); hdr->id = be16toh(hdr->id); hdr->questions = be16toh(hdr->questions); @@ -1193,42 +1260,53 @@ static int rtpmidi_handle_mdns(){ //rfc6762 18.3: opcode != 0 -> ignore //rfc6762 18.11: response code != 0 -> ignore - //TODO Check for ._apple-midi._udp.local. offset = sizeof(dns_header); - LOGPF("ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); + DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); for(u = 0; u < hdr->questions; u++){ - dns_decode_name(buffer, bytes, offset, &name); + if(dns_decode_name(buffer, bytes, offset, &name)){ + LOG("Failed to decode DNS label"); + return 1; + } offset += name.length; offset += sizeof(dns_question); - - LOGPF("Question (%d bytes): %s", name.length, name.name); } + //look for a SRV answer for ._apple-midi._udp.local. for(u = 0; u < hdr->answers; u++){ - dns_decode_name(buffer, bytes, offset, &name); + if(dns_decode_name(buffer, bytes, offset, &name)){ + LOG("Failed to decode DNS label"); + return 1; + } offset += name.length; rr = (dns_rr*) (buffer + offset); offset += sizeof(dns_rr) + be16toh(rr->data); - LOGPF("Answer (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + if(be16toh(rr->rtype) == 33 + && strlen(name.name) > strlen(RTPMIDI_MDNS_DOMAIN) + && !strcmp(name.name + (strlen(name.name) - strlen(RTPMIDI_MDNS_DOMAIN)), RTPMIDI_MDNS_DOMAIN)){ + LOGPF("Found possible peer: %s", name.name); + } } for(u = 0; u < hdr->servers; u++){ - dns_decode_name(buffer, bytes, offset, &name); + if(dns_decode_name(buffer, bytes, offset, &name)){ + LOG("Failed to decode DNS label"); + return 1; + } offset += name.length; rr = (dns_rr*) (buffer + offset); offset += sizeof(dns_rr) + be16toh(rr->data); - - LOGPF("Authority (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); } + //find a matching A/AAAA record in the additional records for(u = 0; u < hdr->additional; u++){ - dns_decode_name(buffer, bytes, offset, &name); + if(dns_decode_name(buffer, bytes, offset, &name)){ + LOG("Failed to decode DNS label"); + return 1; + } offset += name.length; rr = (dns_rr*) (buffer + offset); offset += sizeof(dns_rr) + be16toh(rr->data); - - LOGPF("Additional (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); } } @@ -1386,6 +1464,11 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; + + if(cfg.mdns_fd >= 0 && data->mode == apple && data->title){ + rtpmidi_mdns_detach(data); + } + if(data->fd >= 0){ close(data->fd); } @@ -1394,8 +1477,8 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ close(data->control_fd); } - free(data->session_name); - data->session_name = NULL; + free(data->title); + data->title = NULL; free(data->accept); data->accept = NULL; diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index f55b543..f240586 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -22,8 +22,8 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) #define RTPMIDI_DEFAULT_NAME "MIDIMonster" #define RTPMIDI_SERVICE_INTERVAL 1000 -#define RTPMIDI_MDNS_DOMAIN "_apple-midi._udp.local" -#define RTPMIDI_DNSSD_DOMAIN "_services._dns-sd._udp.local" +#define RTPMIDI_MDNS_DOMAIN "_apple-midi._udp.local." +#define RTPMIDI_DNSSD_DOMAIN "_services._dns-sd._udp.local." #define DNS_POINTER(a) (((a) & 0xC0) == 0xC0) #define DNS_LABEL_LENGTH(a) ((a) & 0x3F) @@ -77,8 +77,8 @@ typedef struct /*_rtmidi_instance_data*/ { uint16_t sequence; //apple-midi config - char* session_name; /* initiator only */ - char* accept; /* participant only */ + char* title; + char* accept; //direct mode config uint8_t learn_peers; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index c188039..64978ae 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -24,8 +24,8 @@ stream, which may lead to inconsistencies during playback. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration | -| `mdns-name` | `computer1` | none | mDNS hostname to announce, also used as AppleMIDI peer name | +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | +| `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances with `title` configuration will be announced via mDNS if set. | #### Instance configuration @@ -48,13 +48,10 @@ Common instance configuration parameters | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | -| `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | -| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times. | -| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). Setting this option makes the instance a session participant | - -Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter -(and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. +| `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated). | +| `title` | `Just Jamming` | none | Session/device name to announce via mDNS. If unset, the instance will not be announced. | +| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). May be specified multiple times. | +| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). | #### Channel specification @@ -85,3 +82,9 @@ rmidi1.ch0.pitch > rmidi2.ch1.pitch ``` #### Known bugs / problems + +The mDNS and DNS-SD implementations in this backends are extremely terse, to the point of violating 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. + +mDNS discovery may announce flawed records when run on a host with multiple active interface. -- cgit v1.2.3 From c52f3d7f48daaaed52277363beb8325e67d67469 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 8 Apr 2020 22:18:45 +0200 Subject: Restructure mDNS discovery --- backends/rtpmidi.c | 146 +++++++++++++++++++++++++++++++--------------------- backends/rtpmidi.h | 2 + backends/rtpmidi.md | 6 +-- 3 files changed, 92 insertions(+), 62 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 7ad3eca..9d84cde 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "rtpmidi" -#define DEBUG +//#define DEBUG #include #include @@ -15,6 +15,9 @@ //TODO learn peer ssrcs //TODO default mode? //TODO internal loop mode +//TODO announce on mdns input +//TODO connect to discovered peers +//TODO push correct addresses to discovery static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -467,8 +470,8 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) LOG("'title' option is only valid for apple mode instances"); return 1; } - if(strchr(value, '.')){ - LOGPF("Invalid instance title %s on %s: titles may not contain periods", value, inst->name); + if(strchr(value, '.') || strlen(value) > 254){ + LOGPF("Invalid instance title %s on %s: Must be shorter than 254 characters, no periods", value, inst->name); return 1; } free(data->title); @@ -689,7 +692,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size accept->token = command->token; accept->ssrc = htobe32(data->ssrc); //add local name to response - //FIXME might want to use the session name in case it is set + //FIXME use instance title instead of mdns_name memcpy(response + sizeof(apple_command), cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME, strlen((cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME)) + 1); sendto(fd, response, sizeof(apple_command) + strlen(cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); @@ -722,7 +725,6 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size else{ //send invite on data fd LOGPF("Instance %s peer accepted on control port, inviting data port", inst->name); - //FIXME limit max length of session name apple_command* invite = (apple_command*) response; invite->res1 = 0xFFFF; invite->command = htobe16(apple_invite); @@ -1100,6 +1102,8 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ srv->port = htobe16(data->control_port); offset += sizeof(dns_rr_srv); + //rfc2782 (srv) says to not compress `target`, rfc6762 (mdns) 18.14 says to + //we dont do it because i dont want to snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); if(dns_encode_name((char*) frame + offset, &name)){ LOGPF("Failed to encode name for %s", frame + offset); @@ -1160,6 +1164,7 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ frame[offset++] = 0x01; frame[offset++] = 0x07; + data->last_announce = mm_timestamp(); free(name.name); return rtpmidi_mdns_broadcast(frame, offset); bail: @@ -1200,8 +1205,8 @@ static int rtpmidi_service(){ if(data->mode == apple){ //mdns discovery - //TODO time-out this properly - if(cfg.mdns_fd >= 0 && data->title){ + if(cfg.mdns_fd >= 0 && data->title + && (!data->last_announce || mm_timestamp() - data->last_announce > RTPMIDI_ANNOUNCE_INTERVAL)){ rtpmidi_mdns_announce(data); } @@ -1236,21 +1241,80 @@ static int rtpmidi_service(){ return 0; } +//TODO bounds check all accesses +static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host){ + dns_rr* rr = NULL; + dns_rr_srv* srv = NULL; + size_t u = 0, offset = sizeof(dns_header); + uint8_t* session_name = NULL; + + for(u = 0; u < hdr->questions; u++){ + if(dns_decode_name(buffer, length, offset, name)){ + LOG("Failed to decode DNS label"); + return 1; + } + offset += name->length; + offset += sizeof(dns_question); + } + + //look for a SRV answer for ._apple-midi._udp.local. + for(u = 0; u < hdr->answers; u++){ + if(dns_decode_name(buffer, length, offset, name)){ + LOG("Failed to decode DNS label"); + return 1; + } + + //store a pointer to the first label in the current path + //since we decoded the name successfully before and dns_decode_name performs bounds checking, this _should_ be ok + session_name = (DNS_POINTER(buffer[offset])) ? buffer + (DNS_LABEL_LENGTH(buffer[offset]) << 8 | buffer[offset + 1]) : buffer + offset; + + offset += name->length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr); + + if(be16toh(rr->rtype) == 33 + && strlen(name->name) > strlen(RTPMIDI_MDNS_DOMAIN) + && !strcmp(name->name + (strlen(name->name) - strlen(RTPMIDI_MDNS_DOMAIN)), RTPMIDI_MDNS_DOMAIN)){ + //decode the srv data + srv = (dns_rr_srv*) (buffer + offset); + offset += sizeof(dns_rr_srv); + + if(dns_decode_name(buffer, length, offset, host)){ + LOG("Failed to decode SRV target"); + return 1; + } + + if(!strncmp(host->name, cfg.mdns_name, strlen(cfg.mdns_name)) && host->name[strlen(cfg.mdns_name)] == '.'){ + //ignore loopback packets, we don't care about them + return 0; + } + + //we just use the packet's source as peer, because who would announce mdns for another host (also implementing an additional registry for this would bloat this backend further) + LOGPF("Detected possible peer %.*s on %s Port %d", session_name[0], session_name + 1, host->name, be16toh(srv->port)); + offset -= sizeof(dns_rr_srv); + } + + offset += be16toh(rr->data); + } + + + return 0; +} + static int rtpmidi_handle_mdns(){ uint8_t buffer[RTPMIDI_PACKET_BUFFER]; dns_header* hdr = (dns_header*) buffer; - dns_rr* rr = NULL; dns_name name = { .alloc = 0 - }; + }, host = name; ssize_t bytes = 0; - size_t u = 0, offset = 0; for(bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0); bytes > 0; bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0)){ if(bytes < sizeof(dns_header)){ continue; } + //decode basic header hdr->id = be16toh(hdr->id); hdr->questions = be16toh(hdr->questions); hdr->answers = be16toh(hdr->answers); @@ -1260,57 +1324,12 @@ static int rtpmidi_handle_mdns(){ //rfc6762 18.3: opcode != 0 -> ignore //rfc6762 18.11: response code != 0 -> ignore - offset = sizeof(dns_header); DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); - for(u = 0; u < hdr->questions; u++){ - if(dns_decode_name(buffer, bytes, offset, &name)){ - LOG("Failed to decode DNS label"); - return 1; - } - offset += name.length; - offset += sizeof(dns_question); - } - - //look for a SRV answer for ._apple-midi._udp.local. - for(u = 0; u < hdr->answers; u++){ - if(dns_decode_name(buffer, bytes, offset, &name)){ - LOG("Failed to decode DNS label"); - return 1; - } - offset += name.length; - rr = (dns_rr*) (buffer + offset); - offset += sizeof(dns_rr) + be16toh(rr->data); - - if(be16toh(rr->rtype) == 33 - && strlen(name.name) > strlen(RTPMIDI_MDNS_DOMAIN) - && !strcmp(name.name + (strlen(name.name) - strlen(RTPMIDI_MDNS_DOMAIN)), RTPMIDI_MDNS_DOMAIN)){ - LOGPF("Found possible peer: %s", name.name); - } - } - - for(u = 0; u < hdr->servers; u++){ - if(dns_decode_name(buffer, bytes, offset, &name)){ - LOG("Failed to decode DNS label"); - return 1; - } - offset += name.length; - rr = (dns_rr*) (buffer + offset); - offset += sizeof(dns_rr) + be16toh(rr->data); - } - - //find a matching A/AAAA record in the additional records - for(u = 0; u < hdr->additional; u++){ - if(dns_decode_name(buffer, bytes, offset, &name)){ - LOG("Failed to decode DNS label"); - return 1; - } - offset += name.length; - rr = (dns_rr*) (buffer + offset); - offset += sizeof(dns_rr) + be16toh(rr->data); - } + rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host); } free(name.name); + free(host.name); if(bytes <= 0){ if(errno == EAGAIN){ return 0; @@ -1406,7 +1425,7 @@ static int rtpmidi_start_mdns(){ static int rtpmidi_start(size_t n, instance** inst){ size_t u, p, fds = 0; - rtpmidi_instance_data* data = NULL; + rtpmidi_instance_data* data = NULL, *other = NULL; uint8_t mdns_required = 0; for(u = 0; u < n; u++){ @@ -1435,7 +1454,16 @@ static int rtpmidi_start(size_t n, instance** inst){ data->peer[p].connected = 1; } } - else if(data->mode == apple){ + else if(data->mode == apple && data->title){ + //check for unique title + for(p = 0; p < u; p++){ + other = (rtpmidi_instance_data*) inst[p]->impl; + if(other->mode == apple && other->title + && !strcmp(data->title, other->title)){ + LOGPF("Instance titles are required to be unique to allow for mDNS discovery, conflict between %s and %s", inst[p]->name, inst[u]->name); + return 1; + } + } mdns_required = 1; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index f240586..631c45a 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -24,6 +24,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_SERVICE_INTERVAL 1000 #define RTPMIDI_MDNS_DOMAIN "_apple-midi._udp.local." #define RTPMIDI_DNSSD_DOMAIN "_services._dns-sd._udp.local." +#define RTPMIDI_ANNOUNCE_INTERVAL (60 * 1000) #define DNS_POINTER(a) (((a) & 0xC0) == 0xC0) #define DNS_LABEL_LENGTH(a) ((a) & 0x3F) @@ -79,6 +80,7 @@ typedef struct /*_rtmidi_instance_data*/ { //apple-midi config char* title; char* accept; + uint64_t last_announce; //direct mode config uint8_t learn_peers; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 64978ae..84a7cd6 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -1,7 +1,7 @@ ### The `rtpmidi` backend This backend provides read-write access to RTP MIDI streams, which transfer MIDI data -over the network. +over the network. Notably, it has native support in Apple devices. As the specification for RTP MIDI does not normatively indicate any method for session management, most vendors define their own standards for this. @@ -83,8 +83,8 @@ rmidi1.ch0.pitch > rmidi2.ch1.pitch #### Known bugs / problems -The mDNS and DNS-SD implementations in this backends are extremely terse, to the point of violating the +The mDNS and DNS-SD implementations in this backend are extremely terse, to the point of violating 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. -mDNS discovery may announce flawed records when run on a host with multiple active interface. +mDNS discovery may announce flawed records when run on a host with multiple active interfaces. -- cgit v1.2.3 From 11c0c32ec8d7e9185baf733436c221e6742cea84 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 10 Apr 2020 22:43:34 +0200 Subject: Implement OSC bundle format reception (#56) --- backends/osc.c | 105 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 75 insertions(+), 30 deletions(-) (limited to 'backends') diff --git a/backends/osc.c b/backends/osc.c index f40f3f5..72a7b50 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -10,6 +10,7 @@ /* * TODO * ping method + * bundle output */ #define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4) @@ -720,7 +721,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ //sanity check if(ident.fields.channel >= data->channels || ident.fields.parameter >= data->channel[ident.fields.channel].params){ - LOG("Channel identifier out of range"); + LOG("Channel identifier out of range, possibly an output channel was not pre-configured"); return 1; } @@ -757,7 +758,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ return rv; } -static int osc_process_packet(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){ +static int osc_process_message(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){ osc_instance_data* data = (osc_instance_data*) inst->impl; size_t c, p, offset = 0; osc_parameter_value min, max, cur; @@ -809,15 +810,82 @@ static int osc_process_packet(instance* inst, char* local_path, char* format, ui return 0; } +static int osc_process_packet(instance* inst, uint8_t* buffer, size_t len){ + osc_instance_data* data = (osc_instance_data*) inst->impl; + size_t offset = 0, message_length = len; + char* osc_local = NULL, *osc_fmt = NULL; + uint8_t* osc_data = NULL; + uint32_t* bundle_size = NULL; + uint8_t decode_bundle = 0; + + //bundles need at least a header and timestamp + if(len >= 16 && !memcmp(buffer, "#bundle\0", 8)){ + decode_bundle = 1; + offset = 16; + } + + do{ + if(decode_bundle){ + if(len - offset < 4){ + LOGPF("Failed to decode bundle size: %" PRIsize_t " bytes left at %" PRIsize_t " of %" PRIsize_t, len - offset, offset, len); + break; + } + bundle_size = (uint32_t*) (buffer + offset); + message_length = be32toh(*bundle_size); + DBGPF("Next bundle entry has %" PRIsize_t " bytes", message_length); + offset += 4; + + if(len - offset < message_length){ + LOGPF("Bundle member size out of bounds: %" PRIsize_t " bytes left", len - offset); + break; + } + } + + //check for recursive bundles + if(message_length >= 16 && !memcmp(buffer + offset, "#bundle\0", 8)){ + DBGPF("Recursing into sub-bundle of size %" PRIsize_t " on %s", message_length, inst->name); + osc_process_packet(inst, buffer + offset, message_length); + } + //ignore messages if root filter active + else if(data->root && strncmp((char*) (buffer + offset), data->root, min(message_length, strlen(data->root)))){ + DBGPF("Ignoring message due to active root filter %s: data is for %s", data->root, buffer + offset); + } + else{ + //FIXME all these accesses should be checked against message_length + osc_local = (char*) (buffer + offset + (data->root ? strlen(data->root) : 0)); + osc_fmt = (char*) (buffer + offset + osc_align(strlen((char*) (buffer + offset)) + 1)); + + if(*osc_fmt != ','){ + //invalid format string + LOGPF("Invalid format string in packet for instance %s: %s", inst->name, osc_fmt); + } + else{ + osc_fmt++; + + if(osc_global_config.detect){ + LOGPF("Incoming data: Path %s.%s Format %s", inst->name, osc_local, osc_fmt); + } + + osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); + if(osc_process_message(inst, osc_local, osc_fmt, osc_data, message_length - (osc_data - (uint8_t*) buffer))){ + LOGPF("Failed to process OSC message on %s", inst->name); + } + } + } + + offset += message_length; + } + while(offset < len); + + return 0; +} + static int osc_handle(size_t num, managed_fd* fds){ size_t fd; - char recv_buf[OSC_RECV_BUF]; + uint8_t recv_buf[OSC_RECV_BUF]; instance* inst = NULL; osc_instance_data* data = NULL; ssize_t bytes_read = 0; - char* osc_fmt = NULL; - char* osc_local = NULL; - uint8_t* osc_data = NULL; for(fd = 0; fd < num; fd++){ inst = (instance*) fds[fd].impl; @@ -841,30 +909,7 @@ static int osc_handle(size_t num, managed_fd* fds){ break; } - if(data->root && strncmp(recv_buf, data->root, min(bytes_read, strlen(data->root)))){ - //ignore packet for different root - continue; - } - osc_local = recv_buf + (data->root ? strlen(data->root) : 0); - - osc_fmt = recv_buf + osc_align(strlen(recv_buf) + 1); - if(*osc_fmt != ','){ - //invalid format string - LOGPF("Invalid format string in packet for instance %s", inst->name); - continue; - } - osc_fmt++; - - if(osc_global_config.detect){ - LOGPF("Incoming data: Path %s.%s Format %s", inst->name, osc_local, osc_fmt); - } - - //FIXME check supplied data length - osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); - - if(osc_process_packet(inst, osc_local, osc_fmt, osc_data, bytes_read - (osc_data - (uint8_t*) recv_buf))){ - return 1; - } + osc_process_packet(inst, recv_buf, bytes_read); } while(bytes_read > 0); #ifdef _WIN32 -- cgit v1.2.3 From b53092ac95fa25d0e6e4e4fc3de9531f43038c4f Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 11 Apr 2020 18:40:39 +0200 Subject: Use local interface addresses for mDNS announce --- backends/Makefile | 2 +- backends/libmmbackend.c | 15 ++++ backends/libmmbackend.h | 4 + backends/rtpmidi.c | 218 +++++++++++++++++++++++++++++++++++++++++------- backends/rtpmidi.h | 6 ++ backends/rtpmidi.md | 9 +- 6 files changed, 220 insertions(+), 34 deletions(-) (limited to 'backends') diff --git a/backends/Makefile b/backends/Makefile index c19d9dd..1e66995 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -48,7 +48,7 @@ maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL rtpmidi.so: ADDITIONAL_OBJS += $(BACKEND_LIB) rtpmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) -rtpmidi.dll: LDLIBS += -lws2_32 +rtpmidi.dll: LDLIBS += -lws2_32 -liphlpapi winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) winmidi.dll: LDLIBS += -lwinmm -lws2_32 diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index b9513ac..ab96646 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -1,6 +1,21 @@ #include "libmmbackend.h" #define LOGPF(format, ...) fprintf(stderr, "libmmbe\t" format "\n", __VA_ARGS__) +#define LOG(message) fprintf(stderr, "libmmbe\t%s\n", (message)) + +int mmbackend_strdup(char** dest, char* src){ + if(*dest){ + free(*dest); + } + + *dest = strdup(src); + + if(!*dest){ + LOG("Failed to allocate memory"); + return 1; + } + return 0; +} void mmbackend_parse_hostspec(char* spec, char** host, char** port, char** options){ size_t u = 0; diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index aa0d0f0..cb211a6 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -18,6 +18,10 @@ /*** BACKEND IMPLEMENTATION LIBRARY ***/ +/** Convenience functions **/ + +int mmbackend_strdup(char** dest, char* src); + /** Networking functions **/ /* diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 9d84cde..95a50d2 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -7,9 +7,19 @@ #include #include +//mmbackend pulls in windows.h, required before more specific includes #include "libmmbackend.h" #include "rtpmidi.h" +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + + //#include "../tests/hexdump.c" //TODO learn peer ssrcs @@ -17,7 +27,9 @@ //TODO internal loop mode //TODO announce on mdns input //TODO connect to discovered peers -//TODO push correct addresses to discovery +//TODO refactor cfg.announces +//TODO windows address discovery +//TODO for some reason, the announce packet generates an exception in the wireshark dissector static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -25,6 +37,10 @@ static struct /*_rtpmidi_global*/ { uint8_t detect; uint64_t last_service; + char* mdns_interface; + size_t addresses; + rtpmidi_addr* address; + size_t announces; rtpmidi_announce* announce; } cfg = { @@ -217,6 +233,137 @@ bail: return -1; } +#ifdef _WIN32 +static int rtpmidi_wide_pfxcmp(wchar_t* haystack, char* needle){ + size_t u; + + for(u = 0; haystack[u] && needle[u]; u++){ + if(haystack[u] != needle[u]){ + return 1; + } + } + + if(!haystack[u] && needle[u]){ + return 1; + } + return 0; +} + +//this has become available with vista for some reason... +static char* inet_ntop(int family, uint8_t* data, char* buffer, size_t length){ + switch(family){ + case AF_INET: + snprintf(buffer, length, "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); + break; + case AF_INET6: + snprintf(buffer, length, "%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X", + data[0], data[1], data[2], data[3], + data[4], data[5], data[6], data[7], + data[8], data[9], data[10], data[11], + data[12], data[13], data[14], data[15]); + break; + } + return buffer; +} +#endif + +static int rtpmidi_announce_addrs(){ + char repr[INET6_ADDRSTRLEN + 1]; + union { + struct sockaddr_in* in4; + struct sockaddr_in6* in6; + struct sockaddr* in; + } addr; + + #ifdef _WIN32 + size_t bytes_alloc = 50 * sizeof(IP_ADAPTER_ADDRESSES); + IP_ADAPTER_UNICAST_ADDRESS_LH* unicast_addr = NULL; + IP_ADAPTER_ADDRESSES* addrs = calloc(1, bytes_alloc), *iter = NULL; + if(!addrs){ + LOG("Failed to allocate memory"); + return 1; + } + + if(GetAdaptersAddresses(0, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, + NULL, addrs, (unsigned long*) &bytes_alloc) != ERROR_SUCCESS){ + //FIXME might try to resize the result list and retry at some point... + LOG("Failed to query local interface addresses"); + free(addrs); + return 1; + } + + for(iter = addrs; iter; iter = iter->Next){ + //filter interfaces if requested + if(cfg.mdns_interface && rtpmidi_wide_pfxcmp(iter->FriendlyName, cfg.mdns_interface)){ + continue; + } + + for(unicast_addr = (IP_ADAPTER_UNICAST_ADDRESS_LH*) iter->FirstUnicastAddress; unicast_addr; unicast_addr = unicast_addr->Next){ + addr.in = unicast_addr->Address.lpSockaddr; + if(addr.in->sa_family != AF_INET && addr.in->sa_family != AF_INET6){ + continue; + } + + cfg.address = realloc(cfg.address, (cfg.addresses + 1) * sizeof(rtpmidi_addr)); + if(!cfg.address){ + cfg.addresses = 0; + LOG("Failed to allocate memory"); + return 1; + } + + cfg.address[cfg.addresses].family = addr.in->sa_family; + memcpy(&cfg.address[cfg.addresses].addr, + (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, + (addr.in->sa_family == AF_INET) ? 4 : 16); + LOGPF("mDNS announce address %" PRIsize_t ": %s (from %S)", cfg.addresses, inet_ntop(addr.in->sa_family, cfg.address[cfg.addresses].addr, repr, sizeof(repr)), iter->FriendlyName); + cfg.addresses++; + } + } + + free(addrs); + #else + struct ifaddrs* ifa = NULL, *iter = NULL; + + if(getifaddrs(&ifa)){ + LOGPF("Failed to get adapter address information: %s", strerror(errno)); + return 1; + } + + for(iter = ifa; iter; iter = iter->ifa_next){ + if((!cfg.mdns_interface || !strcmp(cfg.mdns_interface, iter->ifa_name)) + && strcmp(iter->ifa_name, "lo") + && iter->ifa_addr){ + addr.in = iter->ifa_addr; + if(addr.in->sa_family != AF_INET && addr.in->sa_family != AF_INET6){ + continue; + } + + cfg.address = realloc(cfg.address, (cfg.addresses + 1) * sizeof(rtpmidi_addr)); + if(!cfg.address){ + cfg.addresses = 0; + LOG("Failed to allocate memory"); + return 1; + } + + cfg.address[cfg.addresses].family = addr.in->sa_family; + memcpy(&cfg.address[cfg.addresses].addr, + (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, + (addr.in->sa_family == AF_INET) ? 4 : 16); + LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, inet_ntop(addr.in->sa_family, cfg.address[cfg.addresses].addr, repr, sizeof(repr)), iter->ifa_name); + cfg.addresses++; + } + } + + freeifaddrs(ifa); + #endif + + if(!cfg.addresses){ + LOG("Failed to gather local IP addresses for mDNS announce"); + return 1; + } + return 0; +} + static uint32_t rtpmidi_interval(){ return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); } @@ -228,12 +375,15 @@ static int rtpmidi_configure(char* option, char* value){ return 1; } - cfg.mdns_name = strdup(value); - if(!cfg.mdns_name){ - LOG("Failed to allocate memory"); + return mmbackend_strdup(&cfg.mdns_name, value); + } + else if(!strcmp(option, "mdns-interface")){ + if(cfg.mdns_interface){ + LOG("Duplicate mdns-interface assignment"); return 1; } - return 0; + + return mmbackend_strdup(&cfg.mdns_interface, value); } else if(!strcmp(option, "detect")){ cfg.detect = 0; @@ -474,13 +624,7 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) LOGPF("Invalid instance title %s on %s: Must be shorter than 254 characters, no periods", value, inst->name); return 1; } - free(data->title); - data->title = strdup(value); - if(!data->title){ - LOG("Failed to allocate memory"); - return 1; - } - return 0; + return mmbackend_strdup(&data->title, value); } else if(!strcmp(option, "invite")){ if(data->mode != apple){ @@ -495,13 +639,7 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) LOG("'join' option is only valid for apple mode instances"); return 1; } - free(data->accept); - data->accept = strdup(value); - if(!data->accept){ - LOG("Failed to allocate memory"); - return 1; - } - return 0; + return mmbackend_strdup(&data->accept, value); } LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); @@ -1069,6 +1207,7 @@ bail: return 1; } +//FIXME this should not exceed 1500 bytes static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; dns_header* hdr = (dns_header*) frame; @@ -1077,7 +1216,7 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ dns_name name = { .alloc = 0 }; - size_t offset = 0; + size_t offset = 0, host_offset = 0, u = 0; ssize_t bytes = 0; hdr->id = 0; @@ -1085,7 +1224,7 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ hdr->flags[1] = 0; hdr->questions = hdr->servers = 0; hdr->answers = htobe16(4); - hdr->additional = htobe16(1); + hdr->additional = htobe16(cfg.addresses); offset = sizeof(dns_header); //answer 1: SRV FQDN @@ -1150,19 +1289,35 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ frame[offset++] = 0xC0; frame[offset++] = sizeof(dns_header); - //additional 1: A host + //additional 1: first announce addr + host_offset = offset; snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); - bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 1, 1, 120, 4); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, + (cfg.address[0].family == AF_INET) ? 1 : 28, 1, 120, + (cfg.address[0].family == AF_INET) ? 4 : 16); if(bytes < 0){ return 1; } offset += bytes; - //TODO get local addresses here - frame[offset++] = 0x0A; - frame[offset++] = 0x17; - frame[offset++] = 0x01; - frame[offset++] = 0x07; + memcpy(frame + offset, cfg.address[0].addr, (cfg.address[0].family == AF_INET) ? 4 : 16); + offset += (cfg.address[0].family == AF_INET) ? 4 : 16; + + //push all other announce addresses with a pointer + for(u = 1; u < cfg.addresses; u++){ + frame[offset++] = 0xC0 | (host_offset >> 8); + frame[offset++] = host_offset & 0xFF; + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, + (cfg.address[u].family == AF_INET) ? 1 : 28, 1, 120, + (cfg.address[u].family == AF_INET) ? 4 : 16); + if(bytes < 0){ + return 1; + } + offset += bytes; + + memcpy(frame + offset, cfg.address[u].addr, (cfg.address[u].family == AF_INET) ? 4 : 16); + offset += (cfg.address[u].family == AF_INET) ? 4 : 16; + } data->last_announce = mm_timestamp(); free(name.name); @@ -1475,7 +1630,7 @@ static int rtpmidi_start(size_t n, instance** inst){ fds += (data->control_fd >= 0) ? 2 : 1; } - if(mdns_required && rtpmidi_start_mdns()){ + if(mdns_required && (rtpmidi_announce_addrs() || rtpmidi_start_mdns())){ return 1; } else if(mdns_required){ @@ -1529,8 +1684,13 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ cfg.announce = NULL; cfg.announces = 0; + free(cfg.address); + cfg.addresses = 0; + free(cfg.mdns_name); cfg.mdns_name = NULL; + free(cfg.mdns_interface); + cfg.mdns_interface = NULL; if(cfg.mdns_fd >= 0){ close(cfg.mdns_fd); } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 631c45a..9d2c40a 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -92,6 +92,12 @@ typedef struct /*rtpmidi_announced_instance*/ { char** invite; } rtpmidi_announce; +typedef struct /*_rtpmidi_addr*/ { + int family; + //this is actually a fair bit too big, but whatever + uint8_t addr[sizeof(struct sockaddr_storage)]; +} rtpmidi_addr; + enum applemidi_command { apple_invite = 0x494E, //IN apple_accept = 0x4F4B, //OK diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 84a7cd6..d15c9b5 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -22,10 +22,11 @@ stream, which may lead to inconsistencies during playback. #### Global configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | -| `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances with `title` configuration will be announced via mDNS if set. | +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------| +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | +| `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances with `title` configuration will be announced via mDNS if set. | +| `mdns-interface` | `wlan0` | none | Limit addresses announced via mDNS to this interface. On Windows, this is prefix-matched against the user-editable "friendly" interface name. | #### Instance configuration -- cgit v1.2.3 From bac13064352234acea012de30ee36dd51748b97f Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 11 Apr 2020 19:22:57 +0200 Subject: Implement strerror abstraction for Windows --- backends/libmmbackend.c | 21 ++++++++++++++++----- backends/libmmbackend.h | 1 + backends/rtpmidi.c | 19 ++++++++++--------- 3 files changed, 27 insertions(+), 14 deletions(-) (limited to 'backends') diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index ab96646..1624f56 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -17,6 +17,17 @@ int mmbackend_strdup(char** dest, char* src){ return 0; } +char* mmbackend_sockstrerror(int err_no){ + #ifdef _WIN32 + static char error[2048] = ""; + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), error, sizeof(error), NULL); + return error; + #else + return strerror(err_no); + #endif +} + void mmbackend_parse_hostspec(char* spec, char** host, char** port, char** options){ size_t u = 0; @@ -107,18 +118,18 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin //set required socket options yes = 1; if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to enable SO_REUSEADDR on socket: %s", strerror(errno)); + LOGPF("Failed to enable SO_REUSEADDR on socket: %s", mmbackend_sockstrerror(errno)); } if(mcast){ yes = 1; if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to enable SO_BROADCAST on socket: %s", strerror(errno)); + LOGPF("Failed to enable SO_BROADCAST on socket: %s", mmbackend_sockstrerror(errno)); } yes = 0; if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", strerror(errno)); + LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_sockstrerror(errno)); } } @@ -156,7 +167,7 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin #else int flags = fcntl(fd, F_GETFL, 0); if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ - LOGPF("Failed to set socket nonblocking: %s", strerror(errno)); + LOGPF("Failed to set socket nonblocking: %s", mmbackend_sockstrerror(errno)); close(fd); return -1; } @@ -174,7 +185,7 @@ int mmbackend_send(int fd, uint8_t* data, size_t length){ sent = send(fd, data + total, 1, 0); #endif if(sent < 0){ - LOGPF("Failed to send: %s", strerror(errno)); + LOGPF("Failed to send: %s", mmbackend_sockstrerror(errno)); return 1; } total += sent; diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index cb211a6..f487ee1 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -21,6 +21,7 @@ /** Convenience functions **/ int mmbackend_strdup(char** dest, char* src); +char* mmbackend_sockstrerror(int errno); /** Networking functions **/ diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 95a50d2..581b16c 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -28,7 +28,6 @@ //TODO announce on mdns input //TODO connect to discovered peers //TODO refactor cfg.announces -//TODO windows address discovery //TODO for some reason, the announce packet generates an exception in the wireshark dissector static struct /*_rtpmidi_global*/ { @@ -325,7 +324,7 @@ static int rtpmidi_announce_addrs(){ struct ifaddrs* ifa = NULL, *iter = NULL; if(getifaddrs(&ifa)){ - LOGPF("Failed to get adapter address information: %s", strerror(errno)); + LOGPF("Failed to get adapter address information: %s", mmbackend_sockstrerror(errno)); return 1; } @@ -411,7 +410,7 @@ static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, ch } if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ - LOGPF("Failed to fetch data port information: %s", strerror(errno)); + LOGPF("Failed to fetch data port information: %s", mmbackend_sockstrerror(errno)); return 1; } @@ -1242,7 +1241,7 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ offset += sizeof(dns_rr_srv); //rfc2782 (srv) says to not compress `target`, rfc6762 (mdns) 18.14 says to - //we dont do it because i dont want to + //we don't do it because i don't want to snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); if(dns_encode_name((char*) frame + offset, &name)){ LOGPF("Failed to encode name for %s", frame + offset); @@ -1486,11 +1485,15 @@ static int rtpmidi_handle_mdns(){ free(name.name); free(host.name); if(bytes <= 0){ + #ifdef _WIN32 + if(WSAGetLastError() == WSAEWOULDBLOCK){ + #else if(errno == EAGAIN){ + #endif return 0; } - LOGPF("Error reading from mDNS descriptor: %s", strerror(errno)); + LOGPF("Error reading from mDNS descriptor: %s", mmbackend_sockstrerror(errno)); return 1; } @@ -1564,14 +1567,12 @@ static int rtpmidi_start_mdns(){ //join ipv4 multicast group if(setsockopt(cfg.mdns_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ - LOGPF("Failed to join IPv4 multicast group for mDNS: %s", strerror(errno)); - return 1; + LOGPF("Failed to join IPv4 multicast group for mDNS, discovery may be impaired: %s", mmbackend_sockstrerror(errno)); } //join ipv6 multicast group if(setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ - LOGPF("Failed to join IPv6 multicast group for mDNS: %s", strerror(errno)); - return 1; + LOGPF("Failed to join IPv6 multicast group for mDNS, discovery may be impaired: %s", mmbackend_sockstrerror(errno)); } //register mdns fd to core -- cgit v1.2.3 From 9a035724c13396e9ccb3a27287a220a178ec1ab6 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 12 Apr 2020 09:28:05 +0200 Subject: Fix errno shadowing --- backends/libmmbackend.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index f487ee1..969de6f 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -21,7 +21,7 @@ /** Convenience functions **/ int mmbackend_strdup(char** dest, char* src); -char* mmbackend_sockstrerror(int errno); +char* mmbackend_sockstrerror(int err_no); /** Networking functions **/ -- cgit v1.2.3 From dd90f0a2283e6feb36d553b32f96efae0999a916 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 12 Apr 2020 12:11:32 +0200 Subject: Move sockaddr_ntop implementation to libmmbackend --- backends/libmmbackend.c | 50 +++++++++++++++++++++++++++++++++++++++++++------ backends/libmmbackend.h | 4 +++- backends/rtpmidi.c | 47 +++++++++++++++++++--------------------------- backends/rtpmidi.md | 3 +++ 4 files changed, 69 insertions(+), 35 deletions(-) (limited to 'backends') diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 1624f56..2bbc226 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -17,7 +17,7 @@ int mmbackend_strdup(char** dest, char* src){ return 0; } -char* mmbackend_sockstrerror(int err_no){ +char* mmbackend_socket_strerror(int err_no){ #ifdef _WIN32 static char error[2048] = ""; FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), @@ -28,6 +28,44 @@ char* mmbackend_sockstrerror(int err_no){ #endif } +const char* mmbackend_sockaddr_ntop(struct sockaddr* peer, char* buffer, size_t length){ + union { + struct sockaddr* in; + struct sockaddr_in* in4; + struct sockaddr_in6* in6; + } addr; + addr.in = peer; + #ifdef _WIN32 + uint8_t* data = NULL; + #endif + + switch(addr.in->sa_family){ + //inet_ntop has become available in the winapi with vista, but eh. + #ifdef _WIN32 + case AF_INET6: + data = addr.in6->sin6_addr.s6_addr; + snprintf(buffer, length, "%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X", + data[0], data[1], data[2], data[3], + data[4], data[5], data[6], data[7], + data[8], data[9], data[10], data[11], + data[12], data[13], data[14], data[15]); + return buffer; + case AF_INET: + data = (uint8_t*) &(addr.in4->sin_addr.s_addr); + snprintf(buffer, length, "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); + return buffer; + #else + case AF_INET6: + return inet_ntop(addr.in->sa_family, &(addr.in6->sin6_addr), buffer, length); + case AF_INET: + return inet_ntop(addr.in->sa_family, &(addr.in4->sin_addr), buffer, length); + #endif + default: + snprintf(buffer, length, "Socket family not implemented"); + return buffer; + } +} + void mmbackend_parse_hostspec(char* spec, char** host, char** port, char** options){ size_t u = 0; @@ -118,18 +156,18 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin //set required socket options yes = 1; if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to enable SO_REUSEADDR on socket: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to enable SO_REUSEADDR on socket: %s", mmbackend_socket_strerror(errno)); } if(mcast){ yes = 1; if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to enable SO_BROADCAST on socket: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to enable SO_BROADCAST on socket: %s", mmbackend_socket_strerror(errno)); } yes = 0; if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno)); } } @@ -167,7 +205,7 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin #else int flags = fcntl(fd, F_GETFL, 0); if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ - LOGPF("Failed to set socket nonblocking: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to set socket nonblocking: %s", mmbackend_socket_strerror(errno)); close(fd); return -1; } @@ -185,7 +223,7 @@ int mmbackend_send(int fd, uint8_t* data, size_t length){ sent = send(fd, data + total, 1, 0); #endif if(sent < 0){ - LOGPF("Failed to send: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to send: %s", mmbackend_socket_strerror(errno)); return 1; } total += sent; diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 969de6f..557c243 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -5,6 +5,7 @@ #include //#define close closesocket #else +#include #include #include #endif @@ -21,7 +22,8 @@ /** Convenience functions **/ int mmbackend_strdup(char** dest, char* src); -char* mmbackend_sockstrerror(int err_no); +char* mmbackend_socket_strerror(int err_no); +const char* mmbackend_sockaddr_ntop(struct sockaddr* peer, char* buffer, size_t length); /** Networking functions **/ diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 581b16c..5d588bc 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -247,25 +247,9 @@ static int rtpmidi_wide_pfxcmp(wchar_t* haystack, char* needle){ } return 0; } - -//this has become available with vista for some reason... -static char* inet_ntop(int family, uint8_t* data, char* buffer, size_t length){ - switch(family){ - case AF_INET: - snprintf(buffer, length, "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); - break; - case AF_INET6: - snprintf(buffer, length, "%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X", - data[0], data[1], data[2], data[3], - data[4], data[5], data[6], data[7], - data[8], data[9], data[10], data[11], - data[12], data[13], data[14], data[15]); - break; - } - return buffer; -} #endif +//TODO this should be trimmed down a bit static int rtpmidi_announce_addrs(){ char repr[INET6_ADDRSTRLEN + 1]; union { @@ -314,7 +298,7 @@ static int rtpmidi_announce_addrs(){ memcpy(&cfg.address[cfg.addresses].addr, (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, (addr.in->sa_family == AF_INET) ? 4 : 16); - LOGPF("mDNS announce address %" PRIsize_t ": %s (from %S)", cfg.addresses, inet_ntop(addr.in->sa_family, cfg.address[cfg.addresses].addr, repr, sizeof(repr)), iter->FriendlyName); + LOGPF("mDNS announce address %" PRIsize_t ": %s (from %S)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iter->FriendlyName); cfg.addresses++; } } @@ -324,7 +308,7 @@ static int rtpmidi_announce_addrs(){ struct ifaddrs* ifa = NULL, *iter = NULL; if(getifaddrs(&ifa)){ - LOGPF("Failed to get adapter address information: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to get adapter address information: %s", mmbackend_socket_strerror(errno)); return 1; } @@ -348,7 +332,7 @@ static int rtpmidi_announce_addrs(){ memcpy(&cfg.address[cfg.addresses].addr, (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, (addr.in->sa_family == AF_INET) ? 4 : 16); - LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, inet_ntop(addr.in->sa_family, cfg.address[cfg.addresses].addr, repr, sizeof(repr)), iter->ifa_name); + LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iter->ifa_name); cfg.addresses++; } } @@ -410,7 +394,7 @@ static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, ch } if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ - LOGPF("Failed to fetch data port information: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to fetch data port information: %s", mmbackend_socket_strerror(errno)); return 1; } @@ -1396,11 +1380,12 @@ static int rtpmidi_service(){ } //TODO bounds check all accesses -static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host){ +static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host, struct sockaddr* source){ dns_rr* rr = NULL; dns_rr_srv* srv = NULL; size_t u = 0, offset = sizeof(dns_header); uint8_t* session_name = NULL; + char peer_name[1024]; for(u = 0; u < hdr->questions; u++){ if(dns_decode_name(buffer, length, offset, name)){ @@ -1444,7 +1429,7 @@ static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hd } //we just use the packet's source as peer, because who would announce mdns for another host (also implementing an additional registry for this would bloat this backend further) - LOGPF("Detected possible peer %.*s on %s Port %d", session_name[0], session_name + 1, host->name, be16toh(srv->port)); + LOGPF("Detected possible peer %.*s on %s (%s) Port %d", session_name[0], session_name + 1, host->name, mmbackend_sockaddr_ntop(source, peer_name, sizeof(peer_name)), be16toh(srv->port)); offset -= sizeof(dns_rr_srv); } @@ -1462,8 +1447,12 @@ static int rtpmidi_handle_mdns(){ .alloc = 0 }, host = name; ssize_t bytes = 0; + struct sockaddr_storage peer_addr; + socklen_t peer_len = sizeof(peer_addr); - for(bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0); bytes > 0; bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0)){ + for(bytes = recvfrom(cfg.mdns_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len); + bytes > 0; + bytes = recvfrom(cfg.mdns_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len)){ if(bytes < sizeof(dns_header)){ continue; } @@ -1479,7 +1468,9 @@ static int rtpmidi_handle_mdns(){ //rfc6762 18.11: response code != 0 -> ignore DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); - rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host); + rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr); + + peer_len = sizeof(peer_addr); } free(name.name); @@ -1493,7 +1484,7 @@ static int rtpmidi_handle_mdns(){ return 0; } - LOGPF("Error reading from mDNS descriptor: %s", mmbackend_sockstrerror(errno)); + LOGPF("Error reading from mDNS descriptor: %s", mmbackend_socket_strerror(errno)); return 1; } @@ -1567,12 +1558,12 @@ static int rtpmidi_start_mdns(){ //join ipv4 multicast group if(setsockopt(cfg.mdns_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ - LOGPF("Failed to join IPv4 multicast group for mDNS, discovery may be impaired: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to join IPv4 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno)); } //join ipv6 multicast group if(setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ - LOGPF("Failed to join IPv6 multicast group for mDNS, discovery may be impaired: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to join IPv6 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno)); } //register mdns fd to core diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index d15c9b5..33fbe59 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -89,3 +89,6 @@ specifications in multiple cases. Due to the complexity involved in supporting t arising from this will be considered a bug only in cases where they hinder normal operation of the backend. 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 +enormous size and scope of the protocols and implementations required to make this work. -- cgit v1.2.3 From fe6a11b69fdc3ea7c01f8ffca0c0aa26abe6d9ca Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 13 Apr 2020 22:41:29 +0200 Subject: Refactor local address query, document libmmbackend calls --- backends/libmmbackend.h | 22 ++++++++ backends/rtpmidi.c | 133 ++++++++++++++++++------------------------------ backends/rtpmidi.h | 6 +-- 3 files changed, 75 insertions(+), 86 deletions(-) (limited to 'backends') diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 557c243..08f03aa 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -21,8 +21,30 @@ /** Convenience functions **/ +/* + * Duplicate src into *dest, freeing earlier content of *dest if present + * On success, 0 is returned + * On failure, a message is printed, *dest is a NULL pointer and 1 is returned + */ int mmbackend_strdup(char** dest, char* src); + +/* + * Return a formatted error message pertaining to the last socket operation. + * On Linux/OSX, this calls through to strerror using the provided err_no. + * On Windows, err_no is ignored and WSAGetLastError is called to retrieve + * the status of the last operation. This information is then processed via + * FormatMessage into a fixed buffer, which is returned. Thus, this function + * is not thread-safe on Windows. On Linux, refer to strerror's documentation + * for information on thread-safety. + */ char* mmbackend_socket_strerror(int err_no); + +/* + * Wrap / reimplement (on Windows) inet_ntop to work with struct sockaddr* directly. + * Prints the address in a "human-readable" form into buffer. + * Will modify at most length bytes into buffer, output will be zero-terminated. + * This function only works with AF_INET and AF_INET6 addresses. + */ const char* mmbackend_sockaddr_ntop(struct sockaddr* peer, char* buffer, size_t length); /** Networking functions **/ diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 5d588bc..b7d938b 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -19,7 +19,6 @@ #include #endif - //#include "../tests/hexdump.c" //TODO learn peer ssrcs @@ -27,29 +26,34 @@ //TODO internal loop mode //TODO announce on mdns input //TODO connect to discovered peers -//TODO refactor cfg.announces //TODO for some reason, the announce packet generates an exception in the wireshark dissector static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; + char* mdns_interface; + uint8_t detect; uint64_t last_service; - char* mdns_interface; size_t addresses; rtpmidi_addr* address; - size_t announces; - rtpmidi_announce* announce; + size_t invites; + rtpmidi_invite* invite; } cfg = { .mdns_fd = -1, .mdns_name = NULL, + .mdns_interface = NULL, + .detect = 0, .last_service = 0, - .announces = 0, - .announce = NULL + .addresses = 0, + .address = NULL, + + .invites = 0, + .invite = NULL }; MM_PLUGIN_API int init(){ @@ -232,26 +236,9 @@ bail: return -1; } -#ifdef _WIN32 -static int rtpmidi_wide_pfxcmp(wchar_t* haystack, char* needle){ - size_t u; - - for(u = 0; haystack[u] && needle[u]; u++){ - if(haystack[u] != needle[u]){ - return 1; - } - } - - if(!haystack[u] && needle[u]){ - return 1; - } - return 0; -} -#endif - //TODO this should be trimmed down a bit static int rtpmidi_announce_addrs(){ - char repr[INET6_ADDRSTRLEN + 1]; + char repr[INET6_ADDRSTRLEN + 1] = "", iface[1024] = ""; union { struct sockaddr_in* in4; struct sockaddr_in6* in6; @@ -259,51 +246,27 @@ static int rtpmidi_announce_addrs(){ } addr; #ifdef _WIN32 - size_t bytes_alloc = 50 * sizeof(IP_ADAPTER_ADDRESSES); IP_ADAPTER_UNICAST_ADDRESS_LH* unicast_addr = NULL; - IP_ADAPTER_ADDRESSES* addrs = calloc(1, bytes_alloc), *iter = NULL; - if(!addrs){ - LOG("Failed to allocate memory"); - return 1; - } + IP_ADAPTER_ADDRESSES addrs[50] , *iter = NULL; + size_t bytes_alloc = sizeof(addrs); if(GetAdaptersAddresses(0, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, NULL, addrs, (unsigned long*) &bytes_alloc) != ERROR_SUCCESS){ //FIXME might try to resize the result list and retry at some point... LOG("Failed to query local interface addresses"); - free(addrs); return 1; } for(iter = addrs; iter; iter = iter->Next){ + //friendlyname is a wide string, print it into interface for basic conversion and to avoid implementing wide string handling + snprintf(iface, sizeof(iface), "%S", iter->FriendlyName); //filter interfaces if requested - if(cfg.mdns_interface && rtpmidi_wide_pfxcmp(iter->FriendlyName, cfg.mdns_interface)){ + if(cfg.mdns_interface && strncmp(iface, cfg.mdns_interface, min(strlen(iface), strlen(cfg.mdns_interface)))){ continue; } for(unicast_addr = (IP_ADAPTER_UNICAST_ADDRESS_LH*) iter->FirstUnicastAddress; unicast_addr; unicast_addr = unicast_addr->Next){ addr.in = unicast_addr->Address.lpSockaddr; - if(addr.in->sa_family != AF_INET && addr.in->sa_family != AF_INET6){ - continue; - } - - cfg.address = realloc(cfg.address, (cfg.addresses + 1) * sizeof(rtpmidi_addr)); - if(!cfg.address){ - cfg.addresses = 0; - LOG("Failed to allocate memory"); - return 1; - } - - cfg.address[cfg.addresses].family = addr.in->sa_family; - memcpy(&cfg.address[cfg.addresses].addr, - (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, - (addr.in->sa_family == AF_INET) ? 4 : 16); - LOGPF("mDNS announce address %" PRIsize_t ": %s (from %S)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iter->FriendlyName); - cfg.addresses++; - } - } - - free(addrs); #else struct ifaddrs* ifa = NULL, *iter = NULL; @@ -316,7 +279,9 @@ static int rtpmidi_announce_addrs(){ if((!cfg.mdns_interface || !strcmp(cfg.mdns_interface, iter->ifa_name)) && strcmp(iter->ifa_name, "lo") && iter->ifa_addr){ + snprintf(iface, sizeof(iface), "%s", iter->ifa_name); addr.in = iter->ifa_addr; + #endif if(addr.in->sa_family != AF_INET && addr.in->sa_family != AF_INET6){ continue; } @@ -332,11 +297,13 @@ static int rtpmidi_announce_addrs(){ memcpy(&cfg.address[cfg.addresses].addr, (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, (addr.in->sa_family == AF_INET) ? 4 : 16); - LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iter->ifa_name); + + LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iface); cfg.addresses++; } } + #ifndef _WIN32 freeifaddrs(ifa); #endif @@ -474,52 +441,52 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag static int rtpmidi_push_invite(instance* inst, char* peer){ size_t u, p; - //check whether the instance is already in the announce list - for(u = 0; u < cfg.announces; u++){ - if(cfg.announce[u].inst == inst){ + //check whether the instance is already in the inviter list + for(u = 0; u < cfg.invites; u++){ + if(cfg.invite[u].inst == inst){ break; } } - //add to the announce list - if(u == cfg.announces){ - cfg.announce = realloc(cfg.announce, (cfg.announces + 1) * sizeof(rtpmidi_announce)); - if(!cfg.announce){ + //add to the inviter list + if(u == cfg.invites){ + cfg.invite = realloc(cfg.invite, (cfg.invites + 1) * sizeof(rtpmidi_invite)); + if(!cfg.invite){ LOG("Failed to allocate memory"); - cfg.announces = 0; + cfg.invites = 0; return 1; } - cfg.announce[u].inst = inst; - cfg.announce[u].invites = 0; - cfg.announce[u].invite = NULL; + cfg.invite[u].inst = inst; + cfg.invite[u].invites = 0; + cfg.invite[u].name = NULL; - cfg.announces++; + cfg.invites++; } - //check whether the peer is already in the invite list - for(p = 0; p < cfg.announce[u].invites; p++){ - if(!strcmp(cfg.announce[u].invite[p], peer)){ + //check whether the requested name is already in the invite list for this instance + for(p = 0; p < cfg.invite[u].invites; p++){ + if(!strcmp(cfg.invite[u].name[p], peer)){ return 0; } } //extend the invite list - cfg.announce[u].invite = realloc(cfg.announce[u].invite, (cfg.announce[u].invites + 1) * sizeof(char*)); - if(!cfg.announce[u].invite){ + cfg.invite[u].name = realloc(cfg.invite[u].name, (cfg.invite[u].invites + 1) * sizeof(char*)); + if(!cfg.invite[u].name){ LOG("Failed to allocate memory"); - cfg.announce[u].invites = 0; + cfg.invite[u].invites = 0; return 1; } //append the new invitee - cfg.announce[u].invite[p] = strdup(peer); - if(!cfg.announce[u].invite[p]){ + cfg.invite[u].name[p] = strdup(peer); + if(!cfg.invite[u].name[p]){ LOG("Failed to allocate memory"); return 1; } - cfg.announce[u].invites++; + cfg.invite[u].invites++; return 0; } @@ -1666,15 +1633,15 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ inst[u]->impl = NULL; } - for(u = 0; u < cfg.announces; u++){ - for(p = 0; p < cfg.announce[u].invites; p++){ - free(cfg.announce[u].invite[p]); + for(u = 0; u < cfg.invites; u++){ + for(p = 0; p < cfg.invite[u].invites; p++){ + free(cfg.invite[u].name[p]); } - free(cfg.announce[u].invite); + free(cfg.invite[u].name); } - free(cfg.announce); - cfg.announce = NULL; - cfg.announces = 0; + free(cfg.invite); + cfg.invite = NULL; + cfg.invites = 0; free(cfg.address); cfg.addresses = 0; diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 9d2c40a..de610cd 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -86,11 +86,11 @@ typedef struct /*_rtmidi_instance_data*/ { uint8_t learn_peers; } rtpmidi_instance_data; -typedef struct /*rtpmidi_announced_instance*/ { +typedef struct /*rtpmidi_invited_peer*/ { instance* inst; size_t invites; - char** invite; -} rtpmidi_announce; + char** name; +} rtpmidi_invite; typedef struct /*_rtpmidi_addr*/ { int family; -- cgit v1.2.3 From b34a3f142495da9a2879b6ff13623d9a50dcfb89 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 17 Apr 2020 23:22:16 +0200 Subject: Extend Windows error reporting, refactor apple command transmission --- backends/rtpmidi.c | 105 +++++++++++++++++++++++++++-------------------------- backends/rtpmidi.h | 7 ++-- 2 files changed, 58 insertions(+), 54 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index b7d938b..85665fd 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -238,7 +238,7 @@ bail: //TODO this should be trimmed down a bit static int rtpmidi_announce_addrs(){ - char repr[INET6_ADDRSTRLEN + 1] = "", iface[1024] = ""; + char repr[INET6_ADDRSTRLEN + 1] = "", iface[2048] = ""; union { struct sockaddr_in* in4; struct sockaddr_in6* in6; @@ -247,13 +247,16 @@ static int rtpmidi_announce_addrs(){ #ifdef _WIN32 IP_ADAPTER_UNICAST_ADDRESS_LH* unicast_addr = NULL; - IP_ADAPTER_ADDRESSES addrs[50] , *iter = NULL; + IP_ADAPTER_ADDRESSES addrs[250] , *iter = NULL; size_t bytes_alloc = sizeof(addrs); - if(GetAdaptersAddresses(0, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, - NULL, addrs, (unsigned long*) &bytes_alloc) != ERROR_SUCCESS){ + unsigned long status = GetAdaptersAddresses(0, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, + NULL, addrs, (unsigned long*) &bytes_alloc); + if(status != ERROR_SUCCESS){ //FIXME might try to resize the result list and retry at some point... - LOG("Failed to query local interface addresses"); + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, status, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), iface, sizeof(iface), NULL); + LOGPF("Failed to query local interface addresses (%lu): %s", status, iface); return 1; } @@ -401,11 +404,12 @@ static char* rtpmidi_type_name(uint8_t type){ return "unknown"; } -static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected){ +static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected, ssize_t invite_reference){ size_t u, p = data->peers; for(u = 0; u < data->peers; u++){ //check whether the peer is already in the list + //TODO this probably should take into account the invite_reference (-1 for initiator peers or if unknown but may be present) if(data->peer[u].active && sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ @@ -433,6 +437,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag data->peer[p].active = 1; data->peer[p].learned = learned; data->peer[p].connected = connected; + data->peer[p].invite = invite_reference; data->peer[p].dest = sock_addr; data->peer[p].dest_len = sock_len; return 0; @@ -490,6 +495,37 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ return 0; } +static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + + apple_command* cmd = (apple_command*) &frame; + cmd->res1 = 0xFFFF; + cmd->command = htobe16(command); + cmd->version = htobe32(2); + cmd->token = ((uint32_t) rand()) << 16 | rand(); + cmd->ssrc = htobe32(data->ssrc); + + //append session name to packet + memcpy(frame + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); + + //FIXME should we match sending/receiving ports? if the reference does this, it should be documented + return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1, 0, dest, dest_len); +} + +static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + struct sockaddr_storage dest_addr; + + 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 + ((struct sockaddr_in*) &dest_addr)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &dest_addr)->sin_port) - 1); + } + + return rtpmidi_applecommand(inst, (struct sockaddr*) &dest_addr, data->peer[peer].dest_len, control, command); +} + static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; char* host = NULL, *port = NULL; @@ -563,7 +599,7 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) ((struct sockaddr_in*) &sock_addr)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &sock_addr)->sin_port) + 1); } - return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0); + return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0, -1); } else if(!strcmp(option, "title")){ if(data->mode != apple){ @@ -773,56 +809,36 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size //accept the invitation LOGPF("Instance %s accepting invitation to session %s%s", inst->name, session_name ? session_name : "UNNAMED", (fd == data->control_fd) ? " (control)":""); //send accept message - apple_command* accept = (apple_command*) response; - accept->res1 = 0xFFFF; - accept->command = htobe16(apple_accept); - accept->version = htobe32(2); - accept->token = command->token; - accept->ssrc = htobe32(data->ssrc); - //add local name to response - //FIXME use instance title instead of mdns_name - memcpy(response + sizeof(apple_command), cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME, strlen((cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME)) + 1); - sendto(fd, response, sizeof(apple_command) + strlen(cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); + //TODO accept->token = command->token; + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_accept); //push peer if(fd != data->control_fd){ - return rtpmidi_push_peer(data, *peer, peer_len, 1, 1); + return rtpmidi_push_peer(data, *peer, peer_len, 1, 1, -1); } return 0; } else{ //send reject message LOGPF("Instance %s rejecting invitation to session %s", inst->name, session_name ? session_name : "UNNAMED"); - apple_command reject = { - .res1 = 0xFFFF, - .command = htobe16(apple_reject), - .version = htobe32(2), - .token = command->token, - .ssrc = htobe32(data->ssrc) - }; - sendto(fd, (uint8_t*) &reject, sizeof(apple_command), 0, (struct sockaddr*) peer, peer_len); + //TODO .token = command->token, + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_reject); } return 0; } else if(command->command == apple_accept){ if(fd != data->control_fd){ LOGPF("Instance %s negotiated new peer", inst->name); - return rtpmidi_push_peer(data, *peer, peer_len, 1, 1); + return rtpmidi_push_peer(data, *peer, peer_len, 1, 1, -1); //FIXME store ssrc, start timesync } else{ - //send invite on data fd + //invite peer data port LOGPF("Instance %s peer accepted on control port, inviting data port", inst->name); - apple_command* invite = (apple_command*) response; - invite->res1 = 0xFFFF; - invite->command = htobe16(apple_invite); - invite->version = htobe32(2); - invite->token = command->token; - invite->ssrc = htobe32(data->ssrc); - memcpy(response + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); //calculate data port ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); - sendto(data->fd, response, sizeof(apple_command) + strlen(data->title ? data->title : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); + //send invite + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, 0, apple_invite); } return 0; } @@ -1064,7 +1080,7 @@ static int rtpmidi_handle_data(instance* inst){ if(u == data->peers){ LOGPF("Learned new peer on %s", inst->name); - return rtpmidi_push_peer(data, sock_addr, sock_len, 1, 1); + return rtpmidi_push_peer(data, sock_addr, sock_len, 1, 1, -1); } } return 0; @@ -1281,7 +1297,6 @@ static int rtpmidi_service(){ size_t n, u, p; instance** inst = NULL; rtpmidi_instance_data* data = NULL; - uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; struct sockaddr_storage control_peer; //prepare commands @@ -1294,11 +1309,6 @@ static int rtpmidi_service(){ mm_timestamp() * 10 } }; - apple_command* invite = (apple_command*) &frame; - invite->res1 = 0xFFFF; - invite->command = htobe16(apple_invite); - invite->version = htobe32(2); - invite->token = ((uint32_t) rand()) << 16 | rand(); if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ LOG("Failed to fetch instances"); @@ -1329,14 +1339,7 @@ static int rtpmidi_service(){ else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){ //try to invite pre-defined unconnected applemidi peers DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p); - invite->ssrc = htobe32(data->ssrc); - //calculate remote control port from data port - memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); - ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); - //append session name to packet - memcpy(frame + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); - - sendto(data->control_fd, (char*) invite, sizeof(apple_command) + strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + rtpmidi_peer_applecommand(inst[u], p, 1, apple_invite); } } } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index de610cd..a5d4f4c 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -61,8 +61,9 @@ typedef struct /*_rtpmidi_peer*/ { socklen_t dest_len; //uint32_t ssrc; uint8_t active; //marked for reuse - uint8_t learned; //learned / configured peer + uint8_t learned; //learned / configured peer (learned peers are marked inactive on session shutdown) uint8_t connected; //currently in active session + ssize_t invite; //invite-list index for apple-mode learned peers } rtpmidi_peer; typedef struct /*_rtmidi_instance_data*/ { @@ -98,14 +99,14 @@ typedef struct /*_rtpmidi_addr*/ { uint8_t addr[sizeof(struct sockaddr_storage)]; } rtpmidi_addr; -enum applemidi_command { +typedef enum { apple_invite = 0x494E, //IN apple_accept = 0x4F4B, //OK apple_reject = 0x4E4F, //NO apple_leave = 0x4259, //BY apple_sync = 0x434B, //CK apple_feedback = 0x5253 //RS -}; +} applemidi_command; typedef struct /*_dns_name*/ { size_t alloc; -- cgit v1.2.3 From 5ca868f4bf5a48e0d6bd456c02fe834e80dd9fcf Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 18 Apr 2020 17:34:29 +0200 Subject: Use instance name as mDNS announce name --- backends/rtpmidi.c | 64 ++++++++++++++++++----------------------------------- backends/rtpmidi.h | 1 - backends/rtpmidi.md | 3 +-- 3 files changed, 22 insertions(+), 46 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 85665fd..3e998ee 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -507,10 +507,10 @@ static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, sockl cmd->ssrc = htobe32(data->ssrc); //append session name to packet - memcpy(frame + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); + memcpy(frame + sizeof(apple_command), inst->name, strlen(inst->name) + 1); //FIXME should we match sending/receiving ports? if the reference does this, it should be documented - return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1, 0, dest, dest_len); + return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len); } static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command){ @@ -601,17 +601,6 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0, -1); } - else if(!strcmp(option, "title")){ - if(data->mode != apple){ - LOG("'title' option is only valid for apple mode instances"); - return 1; - } - if(strchr(value, '.') || strlen(value) > 254){ - LOGPF("Invalid instance title %s on %s: Must be shorter than 254 characters, no periods", value, inst->name); - return 1; - } - return mmbackend_strdup(&data->title, value); - } else if(!strcmp(option, "invite")){ if(data->mode != apple){ LOG("'invite' option is only valid for apple mode instances"); @@ -1133,7 +1122,7 @@ static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){ return 0; } -static int rtpmidi_mdns_detach(rtpmidi_instance_data* data){ +static int rtpmidi_mdns_detach(instance* inst){ uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; dns_header* hdr = (dns_header*) frame; dns_rr* rr = NULL; @@ -1159,12 +1148,12 @@ static int rtpmidi_mdns_detach(rtpmidi_instance_data* data){ offset += bytes; //TODO length-checks here - frame[offset++] = strlen(data->title); - memcpy(frame + offset, data->title, strlen(data->title)); - offset += strlen(data->title); + frame[offset++] = strlen(inst->name); + memcpy(frame + offset, inst->name, strlen(inst->name)); + offset += strlen(inst->name); frame[offset++] = 0xC0; frame[offset++] = sizeof(dns_header); - rr->data = htobe16(1 + strlen(data->title) + 2); + rr->data = htobe16(1 + strlen(inst->name) + 2); free(name.name); return rtpmidi_mdns_broadcast(frame, offset); @@ -1174,7 +1163,8 @@ bail: } //FIXME this should not exceed 1500 bytes -static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ +static int rtpmidi_mdns_announce(instance* inst){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; dns_header* hdr = (dns_header*) frame; dns_rr* rr = NULL; @@ -1194,7 +1184,7 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ offset = sizeof(dns_header); //answer 1: SRV FQDN - snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", data->title, RTPMIDI_MDNS_DOMAIN); + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", inst->name, RTPMIDI_MDNS_DOMAIN); bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 33, 1, 120, 0); if(bytes < 0){ goto bail; @@ -1320,9 +1310,9 @@ static int rtpmidi_service(){ if(data->mode == apple){ //mdns discovery - if(cfg.mdns_fd >= 0 && data->title + if(cfg.mdns_fd >= 0 && (!data->last_announce || mm_timestamp() - data->last_announce > RTPMIDI_ANNOUNCE_INTERVAL)){ - rtpmidi_mdns_announce(data); + rtpmidi_mdns_announce(inst[u]); } for(p = 0; p < data->peers; p++){ @@ -1542,8 +1532,8 @@ static int rtpmidi_start_mdns(){ static int rtpmidi_start(size_t n, instance** inst){ size_t u, p, fds = 0; - rtpmidi_instance_data* data = NULL, *other = NULL; - uint8_t mdns_required = 0; + rtpmidi_instance_data* data = NULL; + uint8_t mdns_requested = 0; for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; @@ -1571,17 +1561,8 @@ static int rtpmidi_start(size_t n, instance** inst){ data->peer[p].connected = 1; } } - else if(data->mode == apple && data->title){ - //check for unique title - for(p = 0; p < u; p++){ - other = (rtpmidi_instance_data*) inst[p]->impl; - if(other->mode == apple && other->title - && !strcmp(data->title, other->title)){ - LOGPF("Instance titles are required to be unique to allow for mDNS discovery, conflict between %s and %s", inst[p]->name, inst[u]->name); - return 1; - } - } - mdns_required = 1; + else if(data->mode == apple){ + mdns_requested = 1; } //register fds to core @@ -1592,10 +1573,10 @@ static int rtpmidi_start(size_t n, instance** inst){ fds += (data->control_fd >= 0) ? 2 : 1; } - if(mdns_required && (rtpmidi_announce_addrs() || rtpmidi_start_mdns())){ - return 1; + if(mdns_requested && (rtpmidi_announce_addrs() || rtpmidi_start_mdns())){ + LOG("Failed to set up mDNS discovery, instances may not show up on remote hosts and may not find remote peers"); } - else if(mdns_required){ + else if(mdns_requested){ fds++; } @@ -1610,8 +1591,8 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; - if(cfg.mdns_fd >= 0 && data->mode == apple && data->title){ - rtpmidi_mdns_detach(data); + if(cfg.mdns_fd >= 0 && data->mode == apple){ + rtpmidi_mdns_detach(inst[u]); } if(data->fd >= 0){ @@ -1622,9 +1603,6 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ close(data->control_fd); } - free(data->title); - data->title = NULL; - free(data->accept); data->accept = NULL; diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index a5d4f4c..a6b76d9 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -79,7 +79,6 @@ typedef struct /*_rtmidi_instance_data*/ { uint16_t sequence; //apple-midi config - char* title; char* accept; uint64_t last_announce; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 33fbe59..a0098b0 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -25,7 +25,7 @@ stream, which may lead to inconsistencies during playback. | Option | Example value | Default value | Description | |-----------------------|-----------------------|-----------------------|-----------------------| | `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | -| `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances with `title` configuration will be announced via mDNS if set. | +| `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances will be announced via mDNS if set. | | `mdns-interface` | `wlan0` | none | Limit addresses announced via mDNS to this interface. On Windows, this is prefix-matched against the user-editable "friendly" interface name. | #### Instance configuration @@ -50,7 +50,6 @@ Common instance configuration parameters | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated). | -| `title` | `Just Jamming` | none | Session/device name to announce via mDNS. If unset, the instance will not be announced. | | `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). May be specified multiple times. | | `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). | -- cgit v1.2.3 From 7c1a283c9213dd273d04af72e24b08beb83aec12 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 19 Apr 2020 15:27:28 +0200 Subject: Connect discovered mDNS peers --- backends/rtpmidi.c | 147 +++++++++++++++++++++++++++++++++++++++++++---------- backends/rtpmidi.h | 2 +- 2 files changed, 120 insertions(+), 29 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 3e998ee..443967d 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -24,9 +24,10 @@ //TODO learn peer ssrcs //TODO default mode? //TODO internal loop mode -//TODO announce on mdns input -//TODO connect to discovered peers -//TODO for some reason, the announce packet generates an exception in the wireshark dissector +//TODO for some reason, the announce packet generates an exception in the wireshark dns dissector +//TODO rename and document most functions +//TODO timeout non-responsive peers (connected = 0) to allow discovery to reconnect them +//TODO ipv6-mapped-ipv4 creates problens when connecting on a ipv4-bound instance static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -378,7 +379,7 @@ static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, ch return 1; } - LOGPF("Apple mode instance %s listening on port %d", inst->name, data->control_port); + LOGPF("Apple mode instance %s listening on ports %d (control) and %d (data)", inst->name, data->control_port, data->control_port + 1); } else{ data->control_port = be16toh(((struct sockaddr_in*)&sock_addr)->sin_port); @@ -404,7 +405,7 @@ static char* rtpmidi_type_name(uint8_t type){ return "unknown"; } -static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected, ssize_t invite_reference){ +static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr* sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected, ssize_t invite_reference){ size_t u, p = data->peers; for(u = 0; u < data->peers; u++){ @@ -412,7 +413,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag //TODO this probably should take into account the invite_reference (-1 for initiator peers or if unknown but may be present) if(data->peer[u].active && sock_len == data->peer[u].dest_len - && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + && !memcmp(&data->peer[u].dest, sock_addr, sock_len)){ //if yes, update connection flag (but not learned flag because that doesn't change) data->peer[u].connected = connected; return 0; @@ -438,7 +439,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag data->peer[p].learned = learned; data->peer[p].connected = connected; data->peer[p].invite = invite_reference; - data->peer[p].dest = sock_addr; + memcpy(&(data->peer[p].dest), sock_addr, sock_len); data->peer[p].dest_len = sock_len; return 0; } @@ -495,7 +496,7 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ return 0; } -static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command){ +static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command, uint32_t token){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; @@ -503,7 +504,7 @@ static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, sockl cmd->res1 = 0xFFFF; cmd->command = htobe16(command); cmd->version = htobe32(2); - cmd->token = ((uint32_t) rand()) << 16 | rand(); + cmd->token = token ? token : (((uint32_t) rand()) << 16 | rand()); cmd->ssrc = htobe32(data->ssrc); //append session name to packet @@ -513,17 +514,17 @@ static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, sockl return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len); } -static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command){ +static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command, uint32_t token){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; struct sockaddr_storage dest_addr; 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 - ((struct sockaddr_in*) &dest_addr)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &dest_addr)->sin_port) - 1); + ((struct sockaddr_in*) &dest_addr)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &dest_addr)->sin_port) - 1); } - return rtpmidi_applecommand(inst, (struct sockaddr*) &dest_addr, data->peer[peer].dest_len, control, command); + return rtpmidi_applecommand(inst, (struct sockaddr*) &dest_addr, data->peer[peer].dest_len, control, command, token); } static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ @@ -596,10 +597,10 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) //apple peers are specified using the control port, but we want to store the data port as peer if(data->mode == apple){ - ((struct sockaddr_in*) &sock_addr)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &sock_addr)->sin_port) + 1); + ((struct sockaddr_in*) &sock_addr)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) + 1); } - return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0, -1); + return rtpmidi_push_peer(data, (struct sockaddr*) &sock_addr, sock_len, 0, 0, -1); } else if(!strcmp(option, "invite")){ if(data->mode != apple){ @@ -798,36 +799,34 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size //accept the invitation LOGPF("Instance %s accepting invitation to session %s%s", inst->name, session_name ? session_name : "UNNAMED", (fd == data->control_fd) ? " (control)":""); //send accept message - //TODO accept->token = command->token; - rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_accept); + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_accept, command->token); //push peer if(fd != data->control_fd){ - return rtpmidi_push_peer(data, *peer, peer_len, 1, 1, -1); + return rtpmidi_push_peer(data, (struct sockaddr*) peer, peer_len, 1, 1, -1); } return 0; } else{ //send reject message LOGPF("Instance %s rejecting invitation to session %s", inst->name, session_name ? session_name : "UNNAMED"); - //TODO .token = command->token, - rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_reject); + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_reject, command->token); } return 0; } else if(command->command == apple_accept){ if(fd != data->control_fd){ LOGPF("Instance %s negotiated new peer", inst->name); - return rtpmidi_push_peer(data, *peer, peer_len, 1, 1, -1); + return rtpmidi_push_peer(data, (struct sockaddr*) peer, peer_len, 1, 1, -1); //FIXME store ssrc, start timesync } else{ //invite peer data port LOGPF("Instance %s peer accepted on control port, inviting data port", inst->name); //calculate data port - ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); + ((struct sockaddr_in*) peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) peer)->sin_port) + 1); //send invite - rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, 0, apple_invite); + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, 0, apple_invite, 0); } return 0; } @@ -837,7 +836,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size } else if(command->command == apple_leave){ //remove peer from list - this comes in on the control port, but we need to remove the data port... - ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); + ((struct sockaddr_in*) peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) peer)->sin_port) + 1); for(u = 0; u < data->peers; u++){ if(data->peer[u].dest_len == peer_len && !memcmp(&data->peer[u].dest, peer, peer_len)){ @@ -1069,7 +1068,7 @@ static int rtpmidi_handle_data(instance* inst){ if(u == data->peers){ LOGPF("Learned new peer on %s", inst->name); - return rtpmidi_push_peer(data, sock_addr, sock_len, 1, 1, -1); + return rtpmidi_push_peer(data, (struct sockaddr*) &sock_addr, sock_len, 1, 1, -1); } } return 0; @@ -1322,14 +1321,14 @@ static int rtpmidi_service(){ sync.ssrc = htobe32(data->ssrc); //calculate remote control port from data port memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); - ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); + ((struct sockaddr_in*) &control_peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &control_peer)->sin_port) - 1); sendto(data->control_fd, (char*) &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); } else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){ //try to invite pre-defined unconnected applemidi peers DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p); - rtpmidi_peer_applecommand(inst[u], p, 1, apple_invite); + rtpmidi_peer_applecommand(inst[u], p, 1, apple_invite, 0); } } } @@ -1339,8 +1338,98 @@ static int rtpmidi_service(){ return 0; } +static int rtpmidi_apple_peermatch(uint8_t* session_raw, struct sockaddr* peer, socklen_t peer_len, uint16_t control_port){ + //due to mdns restrictions, session names can at most be 255 characters long + char session_name[1024] = ""; + rtpmidi_instance_data* data = NULL; + size_t u, n, p; + uint8_t done = 0; + + //modify peer to match the data port for the indicated control port + ((struct sockaddr_in*) peer)->sin_port = htobe16(control_port + 1); + snprintf(session_name, sizeof(session_name), "%.*s", session_raw[0], session_raw + 1); + + //find instances that invite exactly this peer + for(u = 0; u < cfg.invites; u++){ + for(n = 0; n < cfg.invite[u].invites; n++){ + if(strlen(cfg.invite[u].name[n]) == session_raw[0] + && !strcmp(cfg.invite[u].name[n], session_name)){ + done = 1; + data = (rtpmidi_instance_data*) cfg.invite[u].inst->impl; + DBGPF("Peer %s explicitly invited on instance %s", session_name, cfg.invite[u].inst->name); + + //check whether this peer (or its equivalent on another protocol) is already connected + for(p = 0; p < data->peers; p++){ + //FIXME might want to scan for explicitly configured peers that match the announced peer + if(data->peer[p].active + && data->peer[p].learned + && data->peer[p].invite == n){ + //we already learned of this peer + break; + } + } + + if(p == data->peers){ + //push a new peer + if(rtpmidi_push_peer(data, peer, peer_len, 1, 0, n)){ + return 1; + } + //find it again + for(p = 0; p < data->peers; p++){ + if(data->peer[p].active + && data->peer[p].learned + && data->peer[p].invite == n){ + //we already learned of this peer + break; + } + } + } + else{ + //if connected, we're done for this instance + //if not, at least the family should match + if(data->peer[p].connected + || data->peer[p].dest.ss_family != peer->sa_family){ + break; + } + + //if not connected and family matches, overwrite + memcpy(&(data->peer[p].dest), peer, data->peer[p].dest_len); + } + + //connect either the pushed or overwritten peer + LOGPF("Inviting peer %s to instance %s", session_name, cfg.invite[u].inst->name); + rtpmidi_peer_applecommand(cfg.invite[u].inst, p, 1, apple_invite, 0); + } + } + } + + //if we found at least one match before, we don't check wildcard invites + if(done){ + return 0; + } + + //find instances with a wildcard invite + for(u = 0; u < cfg.invites; u++){ + for(n = 0; n < cfg.invite[u].invites; n++){ + if(!strcmp(cfg.invite[u].name[n], "*")){ + done = 1; + DBGPF("Peer %.*s implicitly invited on instance %s, converting to explicit invitation", session_name[0], session_name + 1, cfg.invite[u].inst->name); + if(rtpmidi_push_invite(cfg.invite[u].inst, session_name)){ + return 1; + } + } + } + } + + //recurse to connect now-explicit invitations + if(done){ + rtpmidi_apple_peermatch(session_raw, peer, peer_len, control_port); + } + return 0; +} + //TODO bounds check all accesses -static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host, struct sockaddr* source){ +static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host, struct sockaddr* source, socklen_t source_len){ dns_rr* rr = NULL; dns_rr_srv* srv = NULL; size_t u = 0, offset = sizeof(dns_header); @@ -1391,6 +1480,8 @@ static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hd //we just use the packet's source as peer, because who would announce mdns for another host (also implementing an additional registry for this would bloat this backend further) LOGPF("Detected possible peer %.*s on %s (%s) Port %d", session_name[0], session_name + 1, host->name, mmbackend_sockaddr_ntop(source, peer_name, sizeof(peer_name)), be16toh(srv->port)); offset -= sizeof(dns_rr_srv); + + rtpmidi_apple_peermatch(session_name, source, source_len, be16toh(srv->port)); } offset += be16toh(rr->data); @@ -1428,7 +1519,7 @@ static int rtpmidi_handle_mdns(){ //rfc6762 18.11: response code != 0 -> ignore DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); - rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr); + rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr, peer_len); peer_len = sizeof(peer_addr); } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index a6b76d9..9d46911 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -63,7 +63,7 @@ typedef struct /*_rtpmidi_peer*/ { uint8_t active; //marked for reuse uint8_t learned; //learned / configured peer (learned peers are marked inactive on session shutdown) uint8_t connected; //currently in active session - ssize_t invite; //invite-list index for apple-mode learned peers + 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*/ { -- cgit v1.2.3 From ea123f01f769b90b31b4bc78e2bdff5517e7e8de Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 19 Apr 2020 20:48:03 +0200 Subject: Add note to rtpmidi documentation --- backends/rtpmidi.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'backends') diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index a0098b0..2194a05 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -1,7 +1,8 @@ ### The `rtpmidi` backend This backend provides read-write access to RTP MIDI streams, which transfer MIDI data -over the network. Notably, it has native support in Apple devices. +over the network. Notably, RTP MIDI has native support in Apple devices including their +tablets. As the specification for RTP MIDI does not normatively indicate any method for session management, most vendors define their own standards for this. @@ -83,6 +84,9 @@ rmidi1.ch0.pitch > rmidi2.ch1.pitch #### Known bugs / problems +This backend is currently still a work in progress, and is not mentioned on the main README yet for that reason. +Critical feedback and tests across multiple devices are very welcome. + The mDNS and DNS-SD implementations in this backend are extremely terse, to the point of violating 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. -- cgit v1.2.3 From a3a79b824a1936868ba4ab5be53998593b0e9047 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 19 Apr 2020 21:11:28 +0200 Subject: Fix Coverity issues --- backends/rtpmidi.c | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 443967d..af90f27 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -164,7 +164,7 @@ static int dns_encode_name(char* name, dns_name* out){ for(token = strtok_r(name, ".", &save); token; token = strtok_r(NULL, ".", &save)){ //make space for this label, its length and a trailing root label - if(out->alloc < out->length + strlen(token) + 1 + 1){ + if(out->alloc < out->length + strlen(token) + 1 + 1 || !out->name){ out->name = realloc(out->name, (out->length + strlen(token) + 2) * sizeof(char)); if(!out->name){ LOG("Failed to allocate memory"); @@ -319,7 +319,7 @@ static int rtpmidi_announce_addrs(){ } static uint32_t rtpmidi_interval(){ - return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); + return max(0, (int64_t) RTPMIDI_SERVICE_INTERVAL - (int64_t) (mm_timestamp() - cfg.last_service)); } static int rtpmidi_configure(char* option, char* value){ @@ -1526,20 +1526,16 @@ static int rtpmidi_handle_mdns(){ free(name.name); free(host.name); - if(bytes <= 0){ - #ifdef _WIN32 - if(WSAGetLastError() == WSAEWOULDBLOCK){ - #else - if(errno == EAGAIN){ - #endif - return 0; - } - - LOGPF("Error reading from mDNS descriptor: %s", mmbackend_socket_strerror(errno)); - return 1; + #ifdef _WIN32 + if(WSAGetLastError() == WSAEWOULDBLOCK){ + #else + if(errno == EAGAIN){ + #endif + return 0; } - return 0; + LOGPF("Error reading from mDNS descriptor: %s", mmbackend_socket_strerror(errno)); + return 1; } static int rtpmidi_handle(size_t num, managed_fd* fds){ -- cgit v1.2.3 From f053dc36fd515e8d7952e1bdefdc0050226a27c4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 19 Apr 2020 21:28:21 +0200 Subject: Fix unchecked returns --- backends/jack.c | 4 +++- backends/rtpmidi.c | 25 +++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) (limited to 'backends') diff --git a/backends/jack.c b/backends/jack.c index d430c60..c84ed0f 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -205,7 +205,9 @@ static int mmjack_process(jack_nframes_t nframes, void* instp){ //notify the main thread if(mark){ DBGPF("Notifying handler thread for instance %s", inst->name); - send(data->fd, "c", 1, 0); + if(send(data->fd, "c", 1, 0) != 1){ + DBGPF("Failed to notify main thread on %s", inst->name); + } } return rv; } diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index af90f27..6aef5a3 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -19,15 +19,13 @@ #include #endif -//#include "../tests/hexdump.c" - //TODO learn peer ssrcs //TODO default mode? //TODO internal loop mode //TODO for some reason, the announce packet generates an exception in the wireshark dns dissector //TODO rename and document most functions //TODO timeout non-responsive peers (connected = 0) to allow discovery to reconnect them -//TODO ipv6-mapped-ipv4 creates problens when connecting on a ipv4-bound instance +//TODO ipv6-mapped-ipv4 creates problems when connecting on a ipv4-bound instance static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -496,9 +494,10 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ return 0; } -static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command, uint32_t token){ +static int rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command, uint32_t token){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + ssize_t bytes = 0; apple_command* cmd = (apple_command*) &frame; cmd->res1 = 0xFFFF; @@ -511,10 +510,15 @@ static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, sockl memcpy(frame + sizeof(apple_command), inst->name, strlen(inst->name) + 1); //FIXME should we match sending/receiving ports? if the reference does this, it should be documented - return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len); + bytes = sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len); + if(bytes != sizeof(apple_command) + strlen(inst->name) + 1){ + LOGPF("Failed to transmit session command on %s", inst->name); + return 1; + } + return 0; } -static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command, uint32_t token){ +static int rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command, uint32_t token){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; struct sockaddr_storage dest_addr; @@ -875,7 +879,9 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size return 0; } - sendto(fd, response, sizeof(apple_sync_frame), 0, (struct sockaddr*) peer, peer_len); + if(sendto(fd, response, sizeof(apple_sync_frame), 0, (struct sockaddr*) peer, peer_len) != sizeof(apple_sync_frame)){ + LOG("Failed to output sync frame"); + } return 0; } else if(command->command == apple_feedback){ @@ -1116,6 +1122,7 @@ static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){ }; //send to ipv4 and ipv6 mcasts + //FIXME much as it pains me, this should probably be split into two descriptors, one for ipv4 and one for ipv6 sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)); return 0; @@ -1323,7 +1330,9 @@ static int rtpmidi_service(){ memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); ((struct sockaddr_in*) &control_peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &control_peer)->sin_port) - 1); - sendto(data->control_fd, (char*) &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + if(sendto(data->control_fd, (char*) &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len) != sizeof(apple_sync_frame)){ + LOG("Failed to output sync frame"); + } } else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){ //try to invite pre-defined unconnected applemidi peers -- cgit v1.2.3 From 8773fca1f7d2ffed68b6af4967b537d989a1e5b5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 20 Apr 2020 20:38:58 +0200 Subject: Print platform-specific error messages for socket calls --- backends/artnet.c | 15 +++++---------- backends/maweb.c | 2 +- backends/openpixelcontrol.c | 2 +- backends/osc.c | 4 ++-- backends/rtpmidi.c | 15 ++++++++++++++- backends/rtpmidi.md | 4 ++-- backends/sacn.c | 35 ++++++++++++----------------------- 7 files changed, 37 insertions(+), 40 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 585895b..76962b4 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -224,17 +224,12 @@ 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){ - #ifndef _WIN32 - if(errno != EAGAIN){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); - #else + #ifdef _WIN32 if(WSAGetLastError() != WSAEWOULDBLOCK){ - char* error = NULL; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); - LOGPF("Failed to output frame for instance %s: %s", inst->name, error); - LocalFree(error); + #else + if(errno != EAGAIN){ #endif + LOGPF("Failed to output frame for instance %s: %s", inst->name, mmbackend_socket_strerror(errno)); return 1; } //reschedule frame output @@ -414,7 +409,7 @@ static int artnet_handle(size_t num, managed_fd* fds){ #else if(bytes_read < 0 && errno != EAGAIN){ #endif - LOGPF("Failed to receive data: %s", strerror(errno)); + LOGPF("Failed to receive data: %s", mmbackend_socket_strerror(errno)); } if(bytes_read == 0){ diff --git a/backends/maweb.c b/backends/maweb.c index 6861d75..2804508 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -774,7 +774,7 @@ static int maweb_handle_fd(instance* inst){ bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0); if(bytes_read < 0){ - LOGPF("Failed to receive: %s", strerror(errno)); + LOGPF("Failed to receive on %s: %s", inst->name, mmbackend_socket_strerror(errno)); //TODO close, reopen return 1; } diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 2a5e01f..1f8d46a 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -534,7 +534,7 @@ static int openpixel_client_handle(instance* inst, int fd){ ssize_t bytes = recv(fd, buffer, sizeof(buffer), 0); if(bytes <= 0){ if(bytes < 0){ - LOGPF("Failed to receive from client: %s", strerror(errno)); + LOGPF("Failed to receive from client: %s", mmbackend_socket_strerror(errno)); } //close the connection diff --git a/backends/osc.c b/backends/osc.c index 72a7b50..8b552b9 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -696,7 +696,7 @@ static int osc_output_channel(instance* inst, size_t channel){ //output packet if(sendto(data->fd, xmit_buf, offset, 0, (struct sockaddr*) &(data->dest), data->dest_len) < 0){ - LOGPF("Failed to transmit packet: %s", strerror(errno)); + LOGPF("Failed to transmit packet: %s", mmbackend_socket_strerror(errno)); } return 0; } @@ -917,7 +917,7 @@ static int osc_handle(size_t num, managed_fd* fds){ #else if(bytes_read < 0 && errno != EAGAIN){ #endif - LOGPF("Failed to receive data for instance %s: %s", inst->name, strerror(errno)); + LOGPF("Failed to receive data for instance %s: %s", inst->name, mmbackend_socket_strerror(errno)); } if(bytes_read == 0){ diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 6aef5a3..52cb0c5 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -27,6 +27,20 @@ //TODO timeout non-responsive peers (connected = 0) to allow discovery to reconnect them //TODO ipv6-mapped-ipv4 creates problems when connecting on a ipv4-bound instance +/* + * CAVEAT EMPTOR: This is one of the largest backends yet, due to the + * sheer number of protocols involved and their respective complexity. + * The following RFCs may be useful for understanding this backend: + * * RFC 6295 (MIDI Payload for RTP) + * * RFC 1035 (DNS) + * * RFC 6762 (mDNS) + * * RFC 6763 (DNS Service Discovery) + * * RFC 2782 (SRV RR for DNS) + * * To a lesser extent, RFC3550 (RTP) + * Additionally, a strong understanding of the MIDI data stream as well as the details of multicast + * networking for IPv4 and IPv6 are very helpful. + */ + static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; @@ -235,7 +249,6 @@ bail: return -1; } -//TODO this should be trimmed down a bit static int rtpmidi_announce_addrs(){ char repr[INET6_ADDRSTRLEN + 1] = "", iface[2048] = ""; union { diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 2194a05..13fd1f6 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -15,8 +15,8 @@ selectable per-instance, with some methods requiring additional global configura configured in the instance configuration as well as previously unknown peers that voluntarily send data to the instance. * AppleMIDI session management: The instance will be able to communicate (either as participant - or initiator) in an AppleMIDI session, which can optionally be announced via mDNS (better - known as "Bonjour" to Apple users). + or initiator) in an AppleMIDI session, which will be announced via mDNS (better + known as "Bonjour" to Apple users) if possible. Note that instances that receive data from multiple peers will combine all inputs into one stream, which may lead to inconsistencies during playback. diff --git a/backends/sacn.c b/backends/sacn.c index b3376d7..0444949 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -101,7 +101,7 @@ static int sacn_listener(char* host, char* port, uint8_t flags){ if(flags & mcast_loop){ //set IP_MCAST_LOOP to allow local applications to receive output if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to re-enable IP_MULTICAST_LOOP on socket: %s", strerror(errno)); + LOGPF("Failed to re-enable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno)); } } @@ -313,17 +313,12 @@ static int sacn_transmit(instance* inst, sacn_output_universe* output){ memcpy((((uint8_t*)pdu.data.data) + 1), data->data.out, 512); if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ - #ifndef _WIN32 - if(errno != EAGAIN){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); - #else + #ifdef _WIN32 if(WSAGetLastError() != WSAEWOULDBLOCK){ - char* error = NULL; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); - LOGPF("Failed to output frame for instance %s: %s", inst->name, error); - LocalFree(error); + #else + if(errno != EAGAIN){ #endif + LOGPF("Failed to output frame for instance %s: %s", inst->name, mmbackend_socket_strerror(errno)); return 1; } @@ -512,19 +507,13 @@ static void sacn_discovery(size_t fd){ memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t)); if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ - #ifndef _WIN32 - if(errno != EAGAIN){ - LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, strerror(errno)); - } - #else + #ifdef _WIN32 if(WSAGetLastError() != WSAEWOULDBLOCK){ - char* error = NULL; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); - LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, error); - LocalFree(error); - } + #else + if(errno != EAGAIN){ #endif + LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, mmbackend_socket_strerror(errno)); + } } } } @@ -603,7 +592,7 @@ static int sacn_handle(size_t num, managed_fd* fds){ #else if(bytes_read < 0 && errno != EAGAIN){ #endif - LOGPF("Failed to receive data: %s", strerror(errno)); + LOGPF("Failed to receive data: %s", mmbackend_socket_strerror(errno)); } if(bytes_read == 0){ @@ -655,7 +644,7 @@ 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, strerror(errno)); + LOGPF("Failed to join Multicast group for universe %u on instance %s: %s", data->uni, inst[u]->name, mmbackend_socket_strerror(errno)); } } -- cgit v1.2.3 From bc275e10defe27e6d288ccf9125fe9b915168240 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 21 Apr 2020 00:20:23 +0200 Subject: Do not load lua backend automatically on Windows --- backends/Makefile | 2 +- backends/libmmbackend.c | 7 +++++++ backends/lua.md | 5 ++--- backends/rtpmidi.c | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) (limited to 'backends') diff --git a/backends/Makefile b/backends/Makefile index 1e66995..700c9b3 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -65,7 +65,7 @@ ola.so: CPPFLAGS += -Wno-write-strings 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.dll: LDLIBS += -L../libs -llua53 +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\"") diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 2bbc226..92adc3c 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -20,8 +20,15 @@ int mmbackend_strdup(char** dest, char* src){ char* mmbackend_socket_strerror(int err_no){ #ifdef _WIN32 static char error[2048] = ""; + ssize_t u; FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), error, sizeof(error), NULL); + //remove trailing newline that for some reason is included in most of these... + for(u = strlen(error) - 1; u > 0; u--){ + if(!isprint(error[u])){ + error[u] = 0; + } + } return error; #else return strerror(err_no); diff --git a/backends/lua.md b/backends/lua.md index e59e513..0a31dce 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -101,7 +101,6 @@ the MIDIMonster project can not provide this file within this repository. You will need to acquire a copy of `lua53.dll`, for example by downloading it from the [luabinaries project](http://luabinaries.sourceforge.net/download.html). -To build the `lua` backend for Windows, place `lua53.dll` in a subdirectory `libs/` in the project root -and run `make lua.dll` inside the `backends/` directory. - +Place this file in the project root directory and run `make lua.dll` inside the `backends/` directory +to build the backend. At runtime, Windows searches for the file in the same directory as `midimonster.exe`. diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 52cb0c5..7df8563 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -525,7 +525,7 @@ static int rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t //FIXME should we match sending/receiving ports? if the reference does this, it should be documented bytes = sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len); if(bytes != sizeof(apple_command) + strlen(inst->name) + 1){ - LOGPF("Failed to transmit session command on %s", inst->name); + LOGPF("Failed to transmit session command on %s: %s", inst->name, mmbackend_socket_strerror(errno)); return 1; } return 0; -- cgit v1.2.3 From a8be76f20b3eaa20b53e1cda7686845a08cef32b Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 23 Apr 2020 23:38:36 +0200 Subject: Split mDNS socket into v4/v6 --- backends/artnet.c | 2 +- backends/libmmbackend.c | 13 ++++++++---- backends/libmmbackend.h | 2 +- backends/maweb.c | 2 +- backends/openpixelcontrol.c | 4 ++-- backends/osc.c | 2 +- backends/rtpmidi.c | 49 +++++++++++++++++++++++++++------------------ backends/rtpmidi.h | 1 + backends/sacn.c | 2 +- backends/winmidi.c | 2 +- 10 files changed, 47 insertions(+), 32 deletions(-) (limited to 'backends') diff --git a/backends/artnet.c b/backends/artnet.c index 76962b4..e07ea52 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -21,7 +21,7 @@ static int artnet_listener(char* host, char* port){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1, 1); if(fd < 0){ return -1; } diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 92adc3c..186cc66 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -138,7 +138,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad return 0; } -int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast){ +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast, uint8_t dualstack){ int fd = -1, status, yes = 1; struct addrinfo hints = { .ai_family = AF_UNSPEC, @@ -162,18 +162,23 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin //set required socket options yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ + if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*) &yes, sizeof(yes)) < 0){ LOGPF("Failed to enable SO_REUSEADDR on socket: %s", mmbackend_socket_strerror(errno)); } + yes = dualstack ? 0 : 1; + if(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (void*) &yes, sizeof(yes)) < 0){ + LOGPF("Failed to %s dualstack operations on socket: %s", dualstack ? "enable" : "disable", mmbackend_socket_strerror(errno)); + } + if(mcast){ yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ + if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*) &yes, sizeof(yes)) < 0){ LOGPF("Failed to enable SO_BROADCAST on socket: %s", mmbackend_socket_strerror(errno)); } yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ + if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*) &yes, sizeof(yes)) < 0){ LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno)); } } diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 08f03aa..1f0b4d7 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -72,7 +72,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad * Create a socket of given type and mode for a bind / connect host. * Returns -1 on failure, a valid file descriptor for the socket on success. */ -int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast); +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast, uint8_t dualstack); /* * Send arbitrary data over multiple writes if necessary diff --git a/backends/maweb.c b/backends/maweb.c index 2804508..c2f6311 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -639,7 +639,7 @@ static int maweb_connect(instance* inst){ mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); } - data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0); + data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0, 1); if(data->fd < 0){ return 1; } diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 1f8d46a..f2dde23 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -48,7 +48,7 @@ static int openpixel_configure_instance(instance* inst, char* option, char* valu return 1; } - data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0); + data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0, 1); if(data->dest_fd >= 0){ return 0; } @@ -62,7 +62,7 @@ static int openpixel_configure_instance(instance* inst, char* option, char* valu return 1; } - data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0); + data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0, 1); if(data->listen_fd >= 0 && !listen(data->listen_fd, SOMAXCONN)){ return 0; } diff --git a/backends/osc.c b/backends/osc.c index 8b552b9..5887a50 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -525,7 +525,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ } //this requests a socket with SO_BROADCAST set, whether this is useful functionality for OSC is up for debate - data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); + data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1, 1); if(data->fd < 0){ LOGPF("Failed to bind for instance %s", inst->name); return 1; diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 7df8563..eb08e8d 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "rtpmidi" -//#define DEBUG +#define DEBUG #include #include @@ -25,7 +25,6 @@ //TODO for some reason, the announce packet generates an exception in the wireshark dns dissector //TODO rename and document most functions //TODO timeout non-responsive peers (connected = 0) to allow discovery to reconnect them -//TODO ipv6-mapped-ipv4 creates problems when connecting on a ipv4-bound instance /* * CAVEAT EMPTOR: This is one of the largest backends yet, due to the @@ -42,7 +41,10 @@ */ static struct /*_rtpmidi_global*/ { + //mdns is split into v6 and v4 to avoid having to translate ipv6-mapped-ipv4 source addresses int mdns_fd; + int mdns4_fd; + char* mdns_name; char* mdns_interface; @@ -56,6 +58,7 @@ static struct /*_rtpmidi_global*/ { rtpmidi_invite* invite; } cfg = { .mdns_fd = -1, + .mdns4_fd = -1, .mdns_name = NULL, .mdns_interface = NULL, @@ -370,7 +373,7 @@ static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, ch char control_port[32]; //bind to random port if none supplied - data->fd = mmbackend_socket(host, port ? port : "0", SOCK_DGRAM, 1, 0); + data->fd = mmbackend_socket(host, port ? port : "0", SOCK_DGRAM, 1, 0, 1); if(data->fd < 0){ return 1; } @@ -384,7 +387,7 @@ static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, ch if(data->mode == apple){ data->control_port = be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) - 1; snprintf(control_port, sizeof(control_port), "%d", data->control_port); - data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0); + data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0, 1); if(data->control_fd < 0){ LOGPF("Failed to bind control port %s for instance %s", control_port, inst->name); return 1; @@ -1135,9 +1138,8 @@ static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){ }; //send to ipv4 and ipv6 mcasts - //FIXME much as it pains me, this should probably be split into two descriptors, one for ipv4 and one for ipv6 sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); - sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)); + sendto(cfg.mdns4_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)); return 0; } @@ -1329,7 +1331,7 @@ static int rtpmidi_service(){ if(data->mode == apple){ //mdns discovery - if(cfg.mdns_fd >= 0 + if((cfg.mdns_fd >= 0 || cfg.mdns4_fd >= 0) && (!data->last_announce || mm_timestamp() - data->last_announce > RTPMIDI_ANNOUNCE_INTERVAL)){ rtpmidi_mdns_announce(inst[u]); } @@ -1513,7 +1515,7 @@ static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hd return 0; } -static int rtpmidi_handle_mdns(){ +static int rtpmidi_handle_mdns(int fd){ uint8_t buffer[RTPMIDI_PACKET_BUFFER]; dns_header* hdr = (dns_header*) buffer; dns_name name = { @@ -1523,9 +1525,9 @@ static int rtpmidi_handle_mdns(){ struct sockaddr_storage peer_addr; socklen_t peer_len = sizeof(peer_addr); - for(bytes = recvfrom(cfg.mdns_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len); + for(bytes = recvfrom(fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len); bytes > 0; - bytes = recvfrom(cfg.mdns_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len)){ + bytes = recvfrom(fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len)){ if(bytes < sizeof(dns_header)){ continue; } @@ -1540,7 +1542,10 @@ static int rtpmidi_handle_mdns(){ //rfc6762 18.3: opcode != 0 -> ignore //rfc6762 18.11: response code != 0 -> ignore - DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); + DBGPF("%" PRIsize_t " bytes on v%c, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", + bytes, (fd == cfg.mdns_fd ? '6' : '4'), hdr->id, + DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", + hdr->questions, hdr->answers, hdr->servers, hdr->additional); rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr, peer_len); peer_len = sizeof(peer_addr); @@ -1578,7 +1583,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ for(u = 0; u < num; u++){ if(!fds[u].impl){ //handle mDNS discovery input - rtpmidi_handle_mdns(); + rtpmidi_handle_mdns(fds[u].fd); } else{ //handle rtp/control input @@ -1619,24 +1624,25 @@ static int rtpmidi_start_mdns(){ } //FIXME might try passing NULL as host here to work around possible windows ipv6 handicaps - cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); - if(cfg.mdns_fd < 0){ - LOG("Failed to create requested mDNS descriptor"); + cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1, 0); + cfg.mdns4_fd = mmbackend_socket(RTPMIDI_DEFAULT4_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1, 0); + if(cfg.mdns_fd < 0 && cfg.mdns4_fd < 0){ + LOG("Failed to create requested mDNS descriptors"); return 1; } //join ipv4 multicast group - if(setsockopt(cfg.mdns_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ + if(cfg.mdns4_fd >= 0 && setsockopt(cfg.mdns4_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ LOGPF("Failed to join IPv4 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno)); } //join ipv6 multicast group - if(setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ + if(cfg.mdns_fd >= 0 && setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ LOGPF("Failed to join IPv6 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno)); } //register mdns fd to core - return mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL); + return mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL) | mm_manage_fd(cfg.mdns4_fd, BACKEND_NAME, 1, NULL); } static int rtpmidi_start(size_t n, instance** inst){ @@ -1686,7 +1692,7 @@ static int rtpmidi_start(size_t n, instance** inst){ LOG("Failed to set up mDNS discovery, instances may not show up on remote hosts and may not find remote peers"); } else if(mdns_requested){ - fds++; + fds += 2; } LOGPF("Registered %" PRIsize_t " descriptors to core", fds); @@ -1700,7 +1706,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; - if(cfg.mdns_fd >= 0 && data->mode == apple){ + if((cfg.mdns_fd >= 0 || cfg.mdns4_fd >= 0) && data->mode == apple){ rtpmidi_mdns_detach(inst[u]); } @@ -1743,6 +1749,9 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ if(cfg.mdns_fd >= 0){ close(cfg.mdns_fd); } + if(cfg.mdns4_fd >= 0){ + close(cfg.mdns4_fd); + } LOG("Backend shut down"); return 0; diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 9d46911..7e6eccc 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -16,6 +16,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_PACKET_BUFFER 8192 #define RTPMIDI_DEFAULT_HOST "::" +#define RTPMIDI_DEFAULT4_HOST "0.0.0.0" #define RTPMIDI_MDNS_PORT "5353" #define RTPMIDI_HEADER_MAGIC 0x80 #define RTPMIDI_HEADER_TYPE 0x61 diff --git a/backends/sacn.c b/backends/sacn.c index 0444949..0c0fd10 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -80,7 +80,7 @@ static int sacn_listener(char* host, char* port, uint8_t flags){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1, 1); if(fd < 0){ return -1; } diff --git a/backends/winmidi.c b/backends/winmidi.c index 753f25c..030062d 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -438,7 +438,7 @@ static int winmidi_socket_pair(int* fds){ }; //for some reason the feedback connection fails to work on 'real' windows with ipv6 - fds[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); + fds[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0, 0); if(fds[0] < 0){ LOG("Failed to open feedback socket"); return 1; -- cgit v1.2.3 From ff785c5c5a300d01b404c48335de7f68ad8711a9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 24 Apr 2020 00:18:36 +0200 Subject: Update lua documentation, keep last value for handler --- backends/lua.c | 3 ++- backends/lua.md | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) (limited to 'backends') diff --git a/backends/lua.c b/backends/lua.c index d7f2643..98ce369 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -517,7 +517,6 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ //handle all incoming events for(n = 0; n < num; n++){ ident = c[n]->ident; - data->channel[ident].in = v[n].normalised; //call lua channel handlers if present if(data->channel[ident].reference != LUA_NOREF){ //push the channel name @@ -532,6 +531,8 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ lua_pop(data->interpreter, 1); } } + //update the channel input value after the handler call, so we can use both values there + data->channel[ident].in = v[n].normalised; } //clear the channel name diff --git a/backends/lua.md b/backends/lua.md index 0a31dce..4e58ded 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -3,18 +3,21 @@ The `lua` backend provides a flexible programming environment, allowing users to route, generate and manipulate events using the Lua scripting language. -Every instance has its own interpreter state which can be loaded with custom handler scripts. +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 -may be supplied in the configuration. +which will receive events for all incoming channels may be supplied 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). The following functions are provided within the Lua interpreter for interaction with the MIDIMonster | Function | Usage example | Description | |-------------------------------|-------------------------------|---------------------------------------| | `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel on this instance | -| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function | +| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function. Do not call this function from within a Lua thread. | | `cleanup_handler(function)` | `cleanup_handler(shutdown)` | 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. | | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel on this instance | | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel on this instance | @@ -23,18 +26,27 @@ The following functions are provided within the Lua interpreter for interaction | `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 | +While a channel handler executes, calling `input_value` for that channel returns the previous value. Once +the handler returns, the internal buffer is updated. + Example script: ```lua +-- This function is called when there are incoming events on input channel `bar` +-- It outputs half the input value on the channel `foo` function bar(value) output("foo", value / 2); end +-- This function is registered below to execute every second +-- It toggles output channel `bar` every time it is called by storing the next state in the variable `step` step = 0 function toggle() output("bar", step * 1.0); step = (step + 1) % 2; end +-- This function is registered below to run as a Lua thread +-- It loops infinitely and toggles the output channel `narf` every second function run_show() while(true) do sleep(1000); @@ -44,10 +56,12 @@ function run_show() end end +-- This function is registered below to be called when the MIDIMonster shuts down function save_values() -- Store state to a file, for example end +-- Register the functions interval(toggle, 1000) thread(run_show) cleanup_handler(save_values) -- cgit v1.2.3 From 4a575cd3c466abfccf4516f5e9f6eacd534f4dc1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 24 Apr 2020 23:28:24 +0200 Subject: Set MCAST_LOOP/IPV6_V6ONLY only on supported sockets, select outbound multicast interfaces properly --- backends/libmmbackend.c | 4 ++-- backends/rtpmidi.c | 44 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 8 deletions(-) (limited to 'backends') diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 186cc66..bad048c 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -167,7 +167,7 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin } yes = dualstack ? 0 : 1; - if(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (void*) &yes, sizeof(yes)) < 0){ + if(addr_it->ai_family == AF_INET6 && setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (void*) &yes, sizeof(yes)) < 0){ LOGPF("Failed to %s dualstack operations on socket: %s", dualstack ? "enable" : "disable", mmbackend_socket_strerror(errno)); } @@ -178,7 +178,7 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin } yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*) &yes, sizeof(yes)) < 0){ + if(setsockopt(fd, addr_it->ai_family == AF_INET ? IPPROTO_IP : IPPROTO_IPV6, addr_it->ai_family == AF_INET ? IP_MULTICAST_LOOP : IPV6_MULTICAST_LOOP, (void*) &yes, sizeof(yes)) < 0){ LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno)); } } diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index eb08e8d..7c5aa69 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "rtpmidi" -#define DEBUG +//#define DEBUG #include #include @@ -15,6 +15,7 @@ #include #else #include +#include #include #include #endif @@ -47,6 +48,10 @@ static struct /*_rtpmidi_global*/ { char* mdns_name; char* mdns_interface; + #ifdef _WIN32 + unsigned mdns_adapter; + unsigned mdns6_adapter; + #endif uint8_t detect; uint64_t last_service; @@ -283,6 +288,13 @@ static int rtpmidi_announce_addrs(){ continue; } + //for exact matches, use exactly this interface for multicasts + if(!strcmp(iface, cfg.mdns_interface)){ + LOGPF("Using interface %s for mDNS discovery", iface); + cfg.mdns_adapter = iter->IfIndex; + cfg.mdns6_adapter = iter->Ipv6IfIndex; + } + for(unicast_addr = (IP_ADAPTER_UNICAST_ADDRESS_LH*) iter->FirstUnicastAddress; unicast_addr; unicast_addr = unicast_addr->Next){ addr.in = unicast_addr->Address.lpSockaddr; #else @@ -1437,7 +1449,7 @@ static int rtpmidi_apple_peermatch(uint8_t* session_raw, struct sockaddr* peer, for(n = 0; n < cfg.invite[u].invites; n++){ if(!strcmp(cfg.invite[u].name[n], "*")){ done = 1; - DBGPF("Peer %.*s implicitly invited on instance %s, converting to explicit invitation", session_name[0], session_name + 1, cfg.invite[u].inst->name); + DBGPF("Peer %s implicitly invited on instance %s, converting to explicit invitation", session_name, cfg.invite[u].inst->name); if(rtpmidi_push_invite(cfg.invite[u].inst, session_name)){ return 1; } @@ -1524,6 +1536,9 @@ static int rtpmidi_handle_mdns(int fd){ ssize_t bytes = 0; struct sockaddr_storage peer_addr; socklen_t peer_len = sizeof(peer_addr); + #ifdef DEBUG + char peer_name[INET6_ADDRSTRLEN + 1]; + #endif for(bytes = recvfrom(fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len); bytes > 0; @@ -1542,10 +1557,11 @@ static int rtpmidi_handle_mdns(int fd){ //rfc6762 18.3: opcode != 0 -> ignore //rfc6762 18.11: response code != 0 -> ignore - DBGPF("%" PRIsize_t " bytes on v%c, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", + DBGPF("%" PRIsize_t " bytes on v%c, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional, src %s", bytes, (fd == cfg.mdns_fd ? '6' : '4'), hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", - hdr->questions, hdr->answers, hdr->servers, hdr->additional); + hdr->questions, hdr->answers, hdr->servers, hdr->additional, + mmbackend_sockaddr_ntop((struct sockaddr*) &peer_addr, peer_name, sizeof(peer_name))); rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr, peer_len); peer_len = sizeof(peer_addr); @@ -1605,9 +1621,15 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ } static int rtpmidi_start_mdns(){ + //use ip_mreqn where possible, but that renames the interface member + #ifdef _WIN32 struct ip_mreq mcast_req = { - .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xe00000fb)), - .imr_interface.s_addr = INADDR_ANY + .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) 0xe00000fb)) }; struct ipv6_mreq mcast6_req = { @@ -1623,6 +1645,16 @@ static int rtpmidi_start_mdns(){ return 0; } + if(cfg.mdns_interface){ + #ifdef _WIN32 + mcast6_req.ipv6mr_interface = cfg.mdns6_adapter; + mcast_req.imr_interface.s_addr = htobe32(cfg.mdns_adapter); + #else + mcast6_req.ipv6mr_interface = if_nametoindex(cfg.mdns_interface); + mcast_req.imr_ifindex = if_nametoindex(cfg.mdns_interface); + #endif + } + //FIXME might try passing NULL as host here to work around possible windows ipv6 handicaps cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1, 0); cfg.mdns4_fd = mmbackend_socket(RTPMIDI_DEFAULT4_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1, 0); -- cgit v1.2.3 From ee086b47e5171698ed9c221e704120522f6abb73 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 25 Apr 2020 00:23:57 +0200 Subject: Add rtpmidi to README, add example config --- backends/rtpmidi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backends') diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 13fd1f6..82548bf 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -27,7 +27,7 @@ stream, which may lead to inconsistencies during playback. |-----------------------|-----------------------|-----------------------|-----------------------| | `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | | `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances will be announced via mDNS if set. | -| `mdns-interface` | `wlan0` | none | Limit addresses announced via mDNS to this interface. On Windows, this is prefix-matched against the user-editable "friendly" interface name. | +| `mdns-interface` | `wlan0` | none | Limit addresses announced via mDNS to this interface. On Windows, this is prefix-matched against the user-editable "friendly" interface name. If this name matches an interface exactly, discovery uses exactly this device. | #### Instance configuration @@ -84,7 +84,7 @@ rmidi1.ch0.pitch > rmidi2.ch1.pitch #### Known bugs / problems -This backend is currently still a work in progress, and is not mentioned on the main README yet for that reason. +This backend has been in development for a long time due to its complexity. There may still be bugs hidden in there. Critical feedback and tests across multiple devices are very welcome. The mDNS and DNS-SD implementations in this backend are extremely terse, to the point of violating the -- cgit v1.2.3 From 3691faaaf6da4d479c31b0246949be2f4a02b8b5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 25 Apr 2020 23:33:54 +0200 Subject: Implement maweb fallback connections --- backends/maweb.c | 109 +++++++++++++++++++++++++++++++++++++++++------------- backends/maweb.h | 7 +++- backends/maweb.md | 12 +++--- 3 files changed, 95 insertions(+), 33 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index c2f6311..192c69e 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -163,19 +163,29 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){ LOGPF("Invalid host specified for instance %s", inst->name); return 1; } - free(data->host); - data->host = strdup(host); - free(data->port); - data->port = NULL; - if(port){ - data->port = strdup(port); + + data->host = realloc(data->host, (data->hosts + 1) * sizeof(char*)); + data->port = realloc(data->port, (data->hosts + 1) * sizeof(char*)); + + if(!data->host || !data->port){ + LOG("Failed to allocate memory"); + return 1; + } + + data->host[data->hosts] = strdup(host); + data->port[data->hosts] = port ? strdup(port) : NULL; + if(!data->host[data->hosts] || (port && !data->port[data->hosts])){ + LOG("Failed to allocate memory"); + free(data->host[data->hosts]); + free(data->port[data->hosts]); + return 1; } + + data->hosts++; return 0; } else if(!strcmp(option, "user")){ - free(data->user); - data->user = strdup(value); - return 0; + return mmbackend_strdup(&data->user, value); } else if(!strcmp(option, "password")){ #ifndef MAWEB_NO_LIBSSL @@ -629,9 +639,11 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le } static int maweb_connect(instance* inst){ + int rv = 1; maweb_instance_data* data = (maweb_instance_data*) inst->impl; - if(!data->host){ - return 1; + if(!data->host || !data->host[data->next_host]){ + LOGPF("Invalid host configuration on instance %s, host %" PRIsize_t, inst->name, data->next_host + 1); + goto bail; } //unregister old fd from core @@ -639,9 +651,14 @@ static int maweb_connect(instance* inst){ mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); } - data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0, 1); + LOGPF("Connecting to host %" PRIsize_t " of %" PRIsize_t " on %s", data->next_host + 1, data->hosts, inst->name); + + data->fd = mmbackend_socket(data->host[data->next_host], + data->port[data->next_host] ? data->port[data->next_host] : MAWEB_DEFAULT_PORT, + SOCK_STREAM, 0, 0, 1); + if(data->fd < 0){ - return 1; + goto bail; } data->state = ws_new; @@ -654,15 +671,20 @@ static int maweb_connect(instance* inst){ || mmbackend_send_str(data->fd, "Sec-WebSocket-Key: rbEQrXMEvCm4ZUjkj6juBQ==\r\n") || mmbackend_send_str(data->fd, "\r\n")){ LOG("Failed to communicate with peer"); - return 1; + goto bail; } //register new fd if(mm_manage_fd(data->fd, BACKEND_NAME, 1, (void*) inst)){ LOG("Failed to register FD"); - return 1; + goto bail; } - return 0; + + rv = 0; +bail: + data->next_host++; + data->next_host %= data->hosts; + return rv; } static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ @@ -693,6 +715,21 @@ static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ return data->offset + begin; } +static int maweb_establish(instance* inst){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + uint8_t connected = 0; + size_t start = data->next_host; + + do{ + if(!maweb_connect(inst)){ + connected = 1; + break; + } + } while(data->next_host != start); + + return connected ? 0 : 1; +} + static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; size_t header_length = 2; @@ -775,12 +812,18 @@ static int maweb_handle_fd(instance* inst){ bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0); if(bytes_read < 0){ LOGPF("Failed to receive on %s: %s", inst->name, mmbackend_socket_strerror(errno)); - //TODO close, reopen - return 1; + if(maweb_establish(inst)){ + LOGPF("Failed to reconnect with any configured host on instance %s", inst->name); + return 1; + } + return 0; } else if(bytes_read == 0){ - //client closed connection - //TODO try to reopen + //client closed connection, try to reopen the connection + if(maweb_establish(inst)){ + LOGPF("Failed to reconnect with any configured host on instance %s", inst->name); + return 1; + } return 0; } @@ -1003,8 +1046,13 @@ static int maweb_start(size_t n, instance** inst){ maweb_instance_data* data = NULL; for(u = 0; u < n; u++){ - //sort channels data = (maweb_instance_data*) inst[u]->impl; + if(!data->hosts){ + LOGPF("No hosts configured on instance %s", inst[u]->name); + return 1; + } + + //sort channels qsort(data->channel, data->channels, sizeof(maweb_channel_data), channel_comparator); //re-set channel identifiers @@ -1012,9 +1060,9 @@ static int maweb_start(size_t n, instance** inst){ data->channel[p].chan->ident = p; } - if(maweb_connect(inst[u])){ - LOGPF("Failed to open connection for instance %s", inst[u]->name); - free(inst); + //try to connect to any available host + if(maweb_establish(inst[u])){ + LOGPF("Failed to connect to any host configured on instance %s", inst[u]->name); return 1; } } @@ -1027,15 +1075,26 @@ static int maweb_start(size_t n, instance** inst){ } static int maweb_shutdown(size_t n, instance** inst){ - size_t u; + size_t u, p; maweb_instance_data* data = NULL; for(u = 0; u < n; u++){ data = (maweb_instance_data*) inst[u]->impl; + + for(p = 0; p < data->hosts; p++){ + //one of these might have failed to allocate + if(data->host){ + free(data->host[p]); + } + if(data->port){ + free(data->port[p]); + } + } free(data->host); data->host = NULL; free(data->port); data->port = NULL; + free(data->user); data->user = NULL; free(data->pass); diff --git a/backends/maweb.h b/backends/maweb.h index 80835d9..85ca09d 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -79,8 +79,11 @@ typedef struct /*_maweb_channel*/ { } maweb_channel_data; typedef struct /*_maweb_instance_data*/ { - char* host; - char* port; + size_t next_host; + size_t hosts; + char** host; + char** port; + char* user; char* pass; diff --git a/backends/maweb.md b/backends/maweb.md index eddf1a5..1547919 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -18,17 +18,17 @@ 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) | -| `quiet` | `1` | `0` | Turn off some warning messages, for use by experts | +| `interval` | `100` | `50` | Query interval for input data polling (in msec). | +| `quiet` | `1` | `0` | Turn off some warning messages, for use by experts. | #### Instance configuration | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|---------------------------------------------------------------| -| `host` | `10.23.42.21 80` | none | Host address (and optional port) of the MA Web Remote | -| `user` | `midimonster` | none | User for the remote session (GrandMA2) | -| `password` | `midimonster` | `midimonster` | Password for the remote session | -| `cmdline` | `console` | `remote` | Commandline key handling mode (see below) | +| `host` | `10.23.42.21 80` | none | Host address (and optional port) of the MA Web Remote. When specified multiple times, the instance will connect the next address when the current connection fails. | +| `user` | `midimonster` | none | User for the remote session (GrandMA2). | +| `password` | `midimonster` | `midimonster` | Password for the remote session. | +| `cmdline` | `console` | `remote` | Commandline key handling mode (see below). | The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are -- cgit v1.2.3 From 929026130c7866d9b70be7a6cc820f103ae241b4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 26 Apr 2020 22:54:17 +0200 Subject: Periodically retry connecting remotes for maweb --- backends/maweb.c | 50 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 18 deletions(-) (limited to 'backends') diff --git a/backends/maweb.c b/backends/maweb.c index 192c69e..5242f36 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -232,6 +232,7 @@ static int maweb_instance(instance* inst){ } data->fd = -1; + data->state = ws_closed; data->buffer = calloc(MAWEB_RECV_CHUNK, sizeof(uint8_t)); if(!data->buffer){ LOG("Failed to allocate memory"); @@ -350,6 +351,9 @@ 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; return 1; } @@ -649,7 +653,11 @@ static int maweb_connect(instance* inst){ //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; LOGPF("Connecting to host %" PRIsize_t " of %" PRIsize_t " on %s", data->next_host + 1, data->hosts, inst->name); @@ -717,17 +725,15 @@ static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ static int maweb_establish(instance* inst){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; - uint8_t connected = 0; size_t start = data->next_host; do{ if(!maweb_connect(inst)){ - connected = 1; break; } } while(data->next_host != start); - return connected ? 0 : 1; + return data->state != ws_closed ? 0 : 1; } static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ @@ -803,7 +809,7 @@ static int maweb_handle_fd(instance* inst){ data->buffer = realloc(data->buffer, (data->allocated + MAWEB_RECV_CHUNK) * sizeof(uint8_t)); if(!data->buffer){ LOG("Failed to allocate memory"); - return 1; + return -1; } data->allocated += MAWEB_RECV_CHUNK; bytes_left += MAWEB_RECV_CHUNK; @@ -812,19 +818,11 @@ static int maweb_handle_fd(instance* inst){ bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0); if(bytes_read < 0){ LOGPF("Failed to receive on %s: %s", inst->name, mmbackend_socket_strerror(errno)); - if(maweb_establish(inst)){ - LOGPF("Failed to reconnect with any configured host on instance %s", inst->name); - return 1; - } - return 0; + return 1; } else if(bytes_read == 0){ //client closed connection, try to reopen the connection - if(maweb_establish(inst)){ - LOGPF("Failed to reconnect with any configured host on instance %s", inst->name); - return 1; - } - return 0; + return 1; } do{ @@ -844,7 +842,6 @@ static int maweb_handle_fd(instance* inst){ if(bytes_handled < 0){ bytes_handled = data->offset + bytes_read; data->offset = 0; - //TODO close, reopen LOG("Failed to handle incoming data"); return 1; } @@ -990,6 +987,12 @@ static int maweb_keepalive(){ snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"session\":%" PRIu64 "}", data->session); maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } + else if(data->state == ws_closed){ + //try to reconnect to any remote + if(maweb_establish(inst[u])){ + LOGPF("Failed to reconnect to any host on %s, will retry in %d seconds", inst[u]->name, MAWEB_CONNECTION_KEEPALIVE / 1000); + } + } } free(inst); @@ -1024,7 +1027,18 @@ static int maweb_handle(size_t num, managed_fd* fds){ int rv = 0; for(n = 0; n < num; n++){ - rv |= maweb_handle_fd((instance*) fds[n].impl); + rv = maweb_handle_fd((instance*) fds[n].impl); + //try to reconnect soft failures + if(rv == 1 && maweb_establish((instance*) fds[n].impl)){ + //keepalive will retry periodically + LOGPF("Failed to reconnect with any configured host on instance %s", ((instance*) fds[n].impl)->name); + } + else if(rv){ + //propagate critical failures + return rv; + } + //errors handled + rv = 0; } //FIXME all keepalive processing allocates temporary buffers, this might an optimization target @@ -1062,8 +1076,8 @@ static int maweb_start(size_t n, instance** inst){ //try to connect to any available host if(maweb_establish(inst[u])){ + //do not return failure here, keepalive will periodically try to reconnect LOGPF("Failed to connect to any host configured on instance %s", inst[u]->name); - return 1; } } @@ -1107,7 +1121,7 @@ static int maweb_shutdown(size_t n, instance** inst){ data->buffer = NULL; data->offset = data->allocated = 0; - data->state = ws_new; + data->state = ws_closed; free(data->channel); data->channel = NULL; -- cgit v1.2.3 From f692715444c6ddeb47bf87b53acf46798785290a Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 27 Apr 2020 20:54:02 +0200 Subject: Allow access to previous value in python handlers --- backends/lua.md | 4 ++-- backends/maweb.c | 2 +- backends/python.c | 9 +++++---- backends/python.md | 3 +++ 4 files changed, 11 insertions(+), 7 deletions(-) (limited to 'backends') diff --git a/backends/lua.md b/backends/lua.md index 4e58ded..b2f40e0 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -26,8 +26,8 @@ The following functions are provided within the Lua interpreter for interaction | `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 | -While a channel handler executes, calling `input_value` for that channel returns the previous value. Once -the handler returns, the internal buffer is updated. +While a channel handler executes, calling `input_value` for that channel returns the previous value. +The stored value is updated once the handler returns. Example script: ```lua diff --git a/backends/maweb.c b/backends/maweb.c index 5242f36..97d4cea 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -570,7 +570,7 @@ static int maweb_request_playbacks(instance* inst){ item_types, view, data->session); - rv |= maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); DBGPF("Poll request: %s", xmit_buffer); updates_inflight++; } diff --git a/backends/python.c b/backends/python.c index 28b95a9..bd73a20 100644 --- a/backends/python.c +++ b/backends/python.c @@ -559,18 +559,19 @@ static int python_set(instance* inst, size_t num, channel** c, channel_value* v) for(u = 0; u < num; u++){ chan = data->channel + c[u]->ident; - //update input value buffer - chan->in = v[u].normalised; - //call handler if present if(chan->handler){ DBGPF("Calling handler for %s.%s", inst->name, chan->name); data->current_channel = chan; - result = PyObject_CallFunction(chan->handler, "d", chan->in); + result = PyObject_CallFunction(chan->handler, "d", v[u].normalised); Py_XDECREF(result); data->current_channel = NULL; DBGPF("Done with handler for %s.%s", inst->name, chan->name); } + + //update input value buffer after finishing the handler + chan->in = v[u].normalised; + } //release interpreter diff --git a/backends/python.md b/backends/python.md index ab0fb38..a78d972 100644 --- a/backends/python.md +++ b/backends/python.md @@ -26,6 +26,9 @@ The `midimonster` module provides the following functions: | `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 | | `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, +while the argument to the handler is the current value. The stored value is updated after the handler finishes executing. + Example Python module: ```python import socket -- cgit v1.2.3