diff options
46 files changed, 4015 insertions, 1160 deletions
diff --git a/.travis-ci.sh b/.travis-ci.sh index 8008026..8f6a5ca 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -1,68 +1,76 @@ #!/bin/bash -# This script is triggered from the script section of .travis.yml -# It runs the appropriate commands depending on the task requested. +if [ "$TASK" = "spellcheck" ]; then + result=0 + # Create list of files to be spellchecked + spellcheck_files=$(find . -type f | grep -v ".git/") -set -e + # Run spellintian to find spelling errors + sl_results=$(xargs spellintian 2>&1 <<< "$spellcheck_files") -SPELLINGBLACKLIST=$(cat <<-BLACKLIST --wholename "./.git/*" -BLACKLIST -) + sl_errors=$(wc -l <<< "$sl_results") + sl_errors_dups=$((grep "\(duplicate word\)" | wc -l) <<< "$sl_results") + sl_errors_nodups=$((grep -v "\(duplicate word\)" | wc -l) <<< "$sl_results") -if [[ $TASK = 'spellintian' ]]; then - # run spellintian only if it is the requested task, ignoring duplicate words - spellingfiles=$(eval "find ./ -type f -and ! \( \ - $SPELLINGBLACKLIST \ - \) | xargs") - # count the number of spellintian errors, ignoring duplicate words - spellingerrors=$(zrun spellintian $spellingfiles 2>&1 | grep -v "\(duplicate word\)" | wc -l) - if [[ $spellingerrors -ne 0 ]]; then - # print the output for info - zrun spellintian $spellingfiles | grep -v "\(duplicate word\)" - echo "Found $spellingerrors spelling errors via spellintian, ignoring duplicates" - exit 1; + if [ "$sl_errors" -ne 0 ]; then + printf "Spellintian found %s errors (%s spelling, %s duplicate words):\n\n" "$sl_errors" "$sl_errors_nodups" "$sl_errors_dups" + printf "%s\n\n" "$sl_results" + result=1 else - echo "Found $spellingerrors spelling errors via spellintian, ignoring duplicates" - fi; -elif [[ $TASK = 'spellintian-duplicates' ]]; then - # run spellintian only if it is the requested task - spellingfiles=$(eval "find ./ -type f -and ! \( \ - $SPELLINGBLACKLIST \ - \) | xargs") - # count the number of spellintian errors - spellingerrors=$(zrun spellintian $spellingfiles 2>&1 | wc -l) - if [[ $spellingerrors -ne 0 ]]; then - # print the output for info - zrun spellintian $spellingfiles - echo "Found $spellingerrors spelling errors via spellintian" - exit 1; - else - echo "Found $spellingerrors spelling errors via spellintian" - fi; -elif [[ $TASK = 'codespell' ]]; then - # run codespell only if it is the requested task - spellingfiles=$(eval "find ./ -type f -and ! \( \ - $SPELLINGBLACKLIST \ - \) | xargs") - # count the number of codespell errors - spellingerrors=$(zrun codespell --check-filenames --check-hidden --quiet 2 --regex "[a-zA-Z0-9][\\-'a-zA-Z0-9]+[a-zA-Z0-9]" $spellingfiles 2>&1 | wc -l) - if [[ $spellingerrors -ne 0 ]]; then - # print the output for info - zrun codespell --check-filenames --check-hidden --quiet 2 --regex "[a-zA-Z0-9][\\-'a-zA-Z0-9]+[a-zA-Z0-9]" $spellingfiles - echo "Found $spellingerrors spelling errors via codespell" - exit 1; + printf "Spellintian reports no errors\n" + fi + + # Run codespell to find some more + cs_results=$(xargs codespell --quiet 2 <<< "$spellcheck_files" 2>&1) + cs_errors=$(wc -l <<< "$cs_results") + if [ "$cs_errors" -ne 0 ]; then + printf "Codespell found %s errors:\n\n" "$cs_errors" + printf "%s\n\n" "$cs_results" + result=1 else - echo "Found $spellingerrors spelling errors via codespell" - fi; -elif [[ $TASK = 'sanitize' ]]; then + printf "Codespell reports no errors\n" + fi + exit "$result" +elif [ "$TASK" = "codesmell" ]; then + result=0 + + if [ -z "$(which lizard)" ]; then + printf "Installing lizard...\n" + pip3 install lizard + fi + + # Run shellcheck for all shell scripts + printf "Running shellcheck...\n" + shell_files="$(find . -type f -iname \*.sh)" + xargs shellcheck -Cnever -s bash <<< "$shell_files" + if [ "$?" -ne "0" ]; then + result=1 + fi + + # Run cloc for some stats + printf "Code statistics:\n\n" + cloc ./ + + # Run lizard for the project + printf "Running lizard for code complexity analysis\n" + lizard ./ + if [ "$?" -ne "0" ]; then + result=1 + fi + + exit "$result" +elif [ "$TASK" = "sanitize" ]; then # Run sanitized compile travis_fold start "make_sanitize" - make sanitize; + if ! make sanitize; then + exit "$?" + fi travis_fold end "make_sanitize" -elif [[ $TASK = 'windows' ]]; then +elif [ "$TASK" = "windows" ]; then travis_fold start "make_windows" - make windows; + if ! make windows; then + exit "$?" + fi make -C backends lua.dll travis_fold end "make_windows" if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then @@ -72,6 +80,7 @@ elif [[ $TASK = 'windows' ]]; then mkdir ./deployment/docs cp ./midimonster.exe ./deployment/ cp ./backends/*.dll ./deployment/backends/ + cp ./backends/*.dll.disabled ./deployment/backends/ cp ./monster.cfg ./deployment/monster.cfg cp ./backends/*.md ./deployment/docs/ cp -r ./configs ./deployment/ @@ -83,7 +92,9 @@ elif [[ $TASK = 'windows' ]]; then else # Otherwise compile as normal travis_fold start "make" - make full; + if ! make full; then + exit "$?" + fi travis_fold end "make" if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then travis_fold start "deploy_unix" @@ -96,7 +107,7 @@ else cp ./backends/*.md ./deployment/docs/ cp -r ./configs ./deployment/ cd ./deployment - tar czf "midimonster-$(git describe)-$TRAVIS_OS_NAME.tgz" * + tar czf "midimonster-$(git describe)-$TRAVIS_OS_NAME.tgz" ./ find . ! -iname '*.tgz' -delete travis_fold end "deploy_unix" fi diff --git a/.travis.yml b/.travis.yml index b9b6969..4e14339 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,13 +7,11 @@ before_script: - export -f travis_fold script: - - "bash -ex .travis-ci.sh" + - "bash .travis-ci.sh" addons: apt: packages: &base_build - # This is the absolute minimum for configure to pass - # Non C++ based tasks use it so they can run make builtfiles - ccache packages: &core_build # This is all the bits we need to enable all options @@ -36,6 +34,13 @@ addons: packages: &core_build_windows - *core_build - mingw-w64 + packages: &linters + - python3 + - python3-pip + - lintian + - codespell + - shellcheck + - cloc jobs: fast_finish: true @@ -74,30 +79,6 @@ jobs: apt: packages: - *core_build_clang_latest - - os: linux - dist: bionic - env: TASK='spellintian' - addons: - apt: - packages: - - *core_build - - moreutils - - os: linux - dist: bionic - env: TASK='spellintian-duplicates' - addons: - apt: - packages: - - *core_build - - moreutils - - os: linux - dist: bionic - env: TASK='codespell' - addons: - apt: - packages: - - *core_build - - moreutils - os: osx osx_image: xcode10.2 compiler: clang @@ -113,13 +94,27 @@ jobs: compiler: clang env: - TASK='sanitize' + - os: linux + dist: bionic + env: TASK='codesmell' + addons: + apt: + packages: + - *linters + - os: linux + dist: bionic + env: TASK='spellcheck' + addons: + apt: + packages: + - *linters allow_failures: - os: linux dist: bionic - env: TASK='spellintian-duplicates' + env: TASK='codesmell' - os: linux dist: bionic - env: TASK='codespell' + env: TASK='spellcheck' env: global: @@ -136,12 +131,10 @@ cache: before_cache: - ccache -s # see how many hits ccache got -install: - - if [ "$TASK" = "codespell" ]; then pip install --user git+https://github.com/codespell-project/codespell.git; fi - before_install: # Travis clones with --branch, which omits tags. Since we use them for the version string at build time, fetch them - git pull --tags + - printf "This is %s on %s\n" "$(git describe)" "$TRAVIS_OS_NAME" - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi # 'brew install' sometimes returns non-zero for some arcane reason. Executing 'true' resets the exit code and allows Travis to continue building... # Travis seems to have Python 2.7 installed by default, which for some reason prevents pkg-config from reading python3.pc @@ -160,7 +153,16 @@ before_install: #OS X uses something other than $CXX variable - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) ]; then $CXX --version; fi # Download libraries to link with on Windows - - if [ "$TASK" == "windows" ]; then mkdir libs; wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; mv lua53.dll libs; fi + - if [ "$TASK" == "windows" ]; then wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; fi + +notifications: + irc: + channels: + - "irc.hackint.org#midimonster" + on_success: change # default: always + on_failure: always # default: always + nick: mm_ci + use_notice: true deploy: provider: releases diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..79005a9 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,50 @@ +# MIDIMonster development guide + +This document serves as a reference for contributors interested in the low-level implementation +of the MIDIMonster. It is currently a work in progress and will be extended as problems come +up and need solving ;) + +## Basics + +All rules are meant as guidelines. There may be situations where they need to be applied +in spirit rather than by the letter. + +### Architectural guidelines + +* Change in functionality or behaviour requires a change in documentation. +* There is more honor in deleting code than there is in adding code. + * Corollary: Code is a liability, not an asset. + * But: Benchmark the naive implementation before optimizing prematurely. +* The `master` branch must build successfully. Test breaking changes in a branch. +* Commit messages should be in the imperative voice ("When applied, this commit will: "). +* The working language for this repository is english. +* External dependencies are only acceptable when necessary and available from package repositories. + +### Code style + +* Tabs for indentations, spaces for word separation +* Lines may not end in spaces or tabs +* There should be no two consecutive spaces (or spaces intermixed with tabs) +* There should be no two consecutive newlines +* All symbol names in `snake_case` except where mandated by external interfaces +* When possible, prefix symbol names with their "namespace" (ie. the relevant section or module name) +* Variables should be appropriately named for what they do + * The name length should be (positively) correlated with usage + * Loop counters may be one-character letters + * Prefer to name unsigned loop counters `u` and signed ones `i` +* Place comments above the section they are commenting on + * Use inline comments sparingly +* Do not omit '{}' brackets, even if optional (e.g. single-statement conditional bodies) +* Opening braces stay on the same line as the condition + +#### C specific + +* Prefer lazy designated initializers to `memset()` +* Avoid `atoi()`/`itoa()`, use `strto[u]l[l]()` and `snprintf()` +* Avoid unsafe functions without explicit bounds parameters (eg. `strcat()`). + +# Build pipeline + +# Architecture + +# Debugging @@ -16,6 +16,7 @@ Currently, the MIDIMonster supports the following protocols: | ArtNet | Linux, Windows, OSX | Version 4 | [`artnet`](backends/artnet.md) | | Streaming ACN (sACN / E1.31) | Linux, Windows, OSX | | [`sacn`](backends/sacn.md) | | OpenSoundControl (OSC) | Linux, Windows, OSX | | [`osc`](backends/osc.md) | +| RTP-MIDI | Linux, Windows, OSX | AppleMIDI sessions supported | [`rtpmidi`](backends/rtpmidi.md) | | OpenPixelControl | Linux, Windows, OSX | 8 Bit & 16 Bit modes | [`openpixelcontrol`](backends/openpixelcontrol.md) | | evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | | Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | @@ -36,6 +37,7 @@ one protocol into channel(s) on any other (or the same) supported protocol, for * Use an OSC app as a simple lighting controller via ArtNet or sACN * Visualize ArtNet data using OSC tools * Control lighting fixtures or DAWs using gamepad controllers, trackballs, etc ([Example configuration](configs/evdev.cfg)) +* Connect a device speaking RTP MIDI (for example, an iPad) to your computer or lighting console ([Example configuration](configs/rtpmidi.cfg)) * Play games, type, or control your mouse using MIDI controllers ([Example configuration](configs/midi-mouse.cfg)) If you encounter a bug or suspect a problem with a protocol implementation, please @@ -76,6 +78,10 @@ lines of the form `option = value`. Lines starting with a semicolon are treated as comments and ignored. Inline comments are not currently supported. +Configuration files may be included recursively in other configuration files using +the syntax `[include <file>]`. This will read the referenced configuration file as +if it were inserted at that point. + Example configuration files may be found in [configs/](configs/). ### Backend and instance configuration @@ -141,6 +147,7 @@ special information. These documentation files are located in the `backends/` di * [`winmidi` backend documentation](backends/winmidi.md) * [`artnet` backend documentation](backends/artnet.md) * [`sacn` backend documentation](backends/sacn.md) +* [`rtpmidi` backend documentation](backends/rtpmidi.md) * [`evdev` backend documentation](backends/evdev.md) * [`loopback` backend documentation](backends/loopback.md) * [`ola` backend documentation](backends/ola.md) @@ -172,6 +179,10 @@ chmod +x ./installer.sh ./installer.sh ``` +The installer can also be used for automating installations or upgrades by specifying additional +command line arguments. To see a list of valid arguments, run the installer with the +`--help` argument. + The installer script can also update MIDIMonster to the latest version automatically, using a configuration file generated during the installation. To do so, run `midimonster-updater` as root on your system after using the installer. @@ -187,8 +198,9 @@ dpkg -i <file>.deb ### Building from source To build the MIDIMonster directly from the sources, you'll need some libraries that provide -support for the protocols to translate. When building from source, you can also to exclude -backends (for example, if you don't need them or don't want to install their prerequisites). +support for the protocols to translate. When building from source, you can also choose to +exclude backends (for example, if you don't need them or don't want to install their +prerequisites). * `libasound2-dev` (for the ALSA MIDI backend) * `libevdev-dev` (for the evdev backend) @@ -1,9 +1,6 @@ keepalive channels per backend? Note source in channel value struct -Optimize core channel search (store backend offset) udp backends may ignore MTU -mm_managed_fd.impl is not freed currently (and is heaped most of the time anyway) -> documentation make event collectors threadsafe to stop marshalling data... collect & check backend API version -windows strerror move all connection establishment to _start to be able to hot-stop/start all backends @@ -4,26 +4,44 @@ #else #define MM_API __attribute__((dllexport)) #endif +#define BACKEND_NAME "core/be" #include "midimonster.h" #include "backend.h" -static size_t nbackends = 0; -static backend* backends = NULL; -static size_t ninstances = 0; -static instance** instances = NULL; -static size_t nchannels = 0; -static channel** channels = NULL; +static struct { + size_t n; + backend* backends; + instance*** instances; +} registry = { + .n = 0 +}; + +//the global channel store was converted from a naive list to a hashmap of lists for performance reasons +static struct { + //channelstore hash is set up for 256 buckets + size_t n[256]; + channel** entry[256]; +} channels = { + .n = { + 0 + } +}; + +static size_t channelstore_hash(instance* inst, uint64_t ident){ + uint64_t repr = ((uint64_t) inst) ^ ident; + return (repr ^ (repr >> 8) ^ (repr >> 16) ^ (repr >> 24) ^ (repr >> 32)) & 0xFF; +} int backends_handle(size_t nfds, managed_fd* fds){ size_t u, p, n; int rv = 0; managed_fd xchg; - for(u = 0; u < nbackends && !rv; u++){ + for(u = 0; u < registry.n && !rv; u++){ n = 0; for(p = 0; p < nfds; p++){ - if(fds[p].backend == backends + u){ + if(fds[p].backend == registry.backends + u){ xchg = fds[n]; fds[n] = fds[p]; fds[p] = xchg; @@ -31,10 +49,13 @@ int backends_handle(size_t nfds, managed_fd* fds){ } } - DBGPF("Notifying backend %s of %lu waiting FDs\n", backends[u].name, n); - rv |= backends[u].process(n, fds); - if(rv){ - fprintf(stderr, "Backend %s failed to handle input\n", backends[u].name); + //handle if there is data ready or the backend has active instances for polling + if(n || registry.instances[u]){ + DBGPF("Notifying backend %s of %" PRIsize_t " waiting FDs\n", registry.backends[u].name, n); + rv |= registry.backends[u].process(n, fds); + if(rv){ + fprintf(stderr, "Backend %s failed to handle input\n", registry.backends[u].name); + } } } return rv; @@ -44,166 +65,149 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ size_t u, p, n; int rv = 0; channel_value xval; - channel* xchnl; + channel* xchnl = NULL; - //TODO eliminate duplicates - for(u = 0; u < ninstances && !rv; u++){ - n = 0; - - for(p = 0; p < nev; p++){ - if(c[p]->instance == instances[u]){ - xval = v[n]; - xchnl = c[n]; + for(u = 0; u < nev && !rv; u = n){ + //sort for this instance + n = u + 1; + for(p = u + 1; p < nev; p++){ + if(c[p]->instance == c[u]->instance){ + xval = v[p]; + xchnl = c[p]; - v[n] = v[p]; - c[n] = c[p]; + v[p] = v[n]; + c[p] = c[n]; - v[p] = xval; - c[p] = xchnl; + v[n] = xval; + c[n] = xchnl; n++; } } - DBGPF("Calling handler for instance %s with %lu events\n", instances[u]->name, n); - rv |= instances[u]->backend->handle(instances[u], n, c, v); + //TODO eliminate duplicates + DBGPF("Calling handler for instance %s with %" PRIsize_t " events\n", c[u]->instance->name, n - u); + rv |= c[u]->instance->backend->handle(c[u]->instance, n - u, c + u, v + u); } return 0; } MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){ - size_t u; - for(u = 0; u < nchannels; u++){ - if(channels[u]->instance == inst && channels[u]->ident == ident){ - DBGPF("Requested channel %lu on instance %s already exists, reusing\n", ident, inst->name); - return channels[u]; + size_t u, bucket = channelstore_hash(inst, ident); + for(u = 0; u < channels.n[bucket]; u++){ + if(channels.entry[bucket][u]->instance == inst + && channels.entry[bucket][u]->ident == ident){ + DBGPF("Requested channel %" PRIu64 " on instance %s already exists, reusing (%" PRIsize_t " search steps)\n", ident, inst->name, u); + return channels.entry[bucket][u]; } } if(!create){ - DBGPF("Requested unknown channel %lu on instance %s\n", ident, inst->name); + DBGPF("Requested unknown channel %" PRIu64 " on instance %s\n", ident, inst->name); return NULL; } - DBGPF("Creating previously unknown channel %lu on instance %s\n", ident, inst->name); - channel** new_chan = realloc(channels, (nchannels + 1) * sizeof(channel*)); - if(!new_chan){ + DBGPF("Creating previously unknown channel %" PRIu64 " on instance %s, bucket %" PRIsize_t "\n", ident, inst->name, bucket); + channels.entry[bucket] = realloc(channels.entry[bucket], (channels.n[bucket] + 1) * sizeof(channel*)); + if(!channels.entry[bucket]){ fprintf(stderr, "Failed to allocate memory\n"); - nchannels = 0; + channels.n[bucket] = 0; return NULL; } - channels = new_chan; - channels[nchannels] = calloc(1, sizeof(channel)); - if(!channels[nchannels]){ + channels.entry[bucket][channels.n[bucket]] = calloc(1, sizeof(channel)); + if(!channels.entry[bucket][channels.n[bucket]]){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; } - channels[nchannels]->instance = inst; - channels[nchannels]->ident = ident; - return channels[nchannels++]; + channels.entry[bucket][channels.n[bucket]]->instance = inst; + channels.entry[bucket][channels.n[bucket]]->ident = ident; + return channels.entry[bucket][(channels.n[bucket]++)]; } -instance* mm_instance(){ - instance** new_inst = realloc(instances, (ninstances + 1) * sizeof(instance*)); - if(!new_inst){ - //TODO free - fprintf(stderr, "Failed to allocate memory\n"); - ninstances = 0; - return NULL; - } - instances = new_inst; - instances[ninstances] = calloc(1, sizeof(instance)); - if(!instances[ninstances]){ - fprintf(stderr, "Failed to allocate memory\n"); - return NULL; - } - - return instances[ninstances++]; -} +instance* mm_instance(backend* b){ + size_t u = 0, n = 0; -MM_API instance* mm_instance_find(char* name, uint64_t ident){ - size_t u; - backend* b = backend_match(name); - if(!b){ - return NULL; - } + for(u = 0; u < registry.n; u++){ + if(registry.backends + u == b){ + //count existing instances + for(n = 0; registry.instances[u] && registry.instances[u][n]; n++){ + } - for(u = 0; u < ninstances; u++){ - if(instances[u]->backend == b && instances[u]->ident == ident){ - return instances[u]; + //extend + registry.instances[u] = realloc(registry.instances[u], (n + 2) * sizeof(instance*)); + if(!registry.instances[u]){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + //sentinel + registry.instances[u][n + 1] = NULL; + registry.instances[u][n] = calloc(1, sizeof(instance)); + if(!registry.instances[u][n]){ + fprintf(stderr, "Failed to allocate memory\n"); + } + registry.instances[u][n]->backend = b; + return registry.instances[u][n]; } } + //this should never happen return NULL; } -MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ - backend* b = backend_match(name); - size_t n = 0, u; - //count number of affected instances - for(u = 0; u < ninstances; u++){ - if(instances[u]->backend == b){ - n++; +MM_API instance* mm_instance_find(char* name, uint64_t ident){ + size_t b = 0; + instance** iter = NULL; + for(b = 0; b < registry.n; b++){ + if(!strcmp(registry.backends[b].name, name)){ + for(iter = registry.instances[b]; iter && *iter; iter++){ + if((*iter)->ident == ident){ + return *iter; + } + } } } - *ninst = n; - - if(!n){ - *inst = NULL; - return 0; - } + return NULL; +} - *inst = calloc(n, sizeof(instance*)); - if(!*inst){ - fprintf(stderr, "Failed to allocate memory\n"); +MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ + size_t b = 0, i = 0; + if(!ninst || !inst){ return 1; } - n = 0; - for(u = 0; u < ninstances; u++){ - if(instances[u]->backend == b){ - (*inst)[n] = instances[u]; - n++; - } - } - return 0; -} + for(b = 0; b < registry.n; b++){ + if(!strcmp(registry.backends[b].name, name)){ + //count instances + for(i = 0; registry.instances[b] && registry.instances[b][i]; i++){ + } -void instances_free(){ - size_t u; - for(u = 0; u < ninstances; u++){ - free(instances[u]->name); - instances[u]->name = NULL; - instances[u]->backend = NULL; - free(instances[u]); - instances[u] = NULL; - } - free(instances); - ninstances = 0; -} + *ninst = i; + if(!i){ + *inst = NULL; + return 0; + } -void channels_free(){ - size_t u; - for(u = 0; u < nchannels; u++){ - DBGPF("Destroying channel %lu on instance %s\n", channels[u]->ident, channels[u]->instance->name); - if(channels[u]->impl && channels[u]->instance->backend->channel_free){ - channels[u]->instance->backend->channel_free(channels[u]); + *inst = calloc(i, sizeof(instance*)); + if(!*inst){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + memcpy(*inst, registry.instances[b], i * sizeof(instance*)); + return 0; } - free(channels[u]); - channels[u] = NULL; } - free(channels); - nchannels = 0; + return 1; } backend* backend_match(char* name){ size_t u; - for(u = 0; u < nbackends; u++){ - if(!strcmp(backends[u].name, name)){ - return backends + u; + for(u = 0; u < registry.n; u++){ + if(!strcmp(registry.backends[u].name, name)){ + return registry.backends + u; } } return NULL; @@ -211,9 +215,12 @@ backend* backend_match(char* name){ instance* instance_match(char* name){ size_t u; - for(u = 0; u < ninstances; u++){ - if(!strcmp(instances[u]->name, name)){ - return instances[u]; + instance** iter = NULL; + for(u = 0; u < registry.n; u++){ + for(iter = registry.instances[u]; iter && *iter; iter++){ + if(!strcmp(name, (*iter)->name)){ + return *iter; + } } } return NULL; @@ -223,14 +230,17 @@ struct timeval backend_timeout(){ size_t u; uint32_t res, secs = 1, msecs = 0; - for(u = 0; u < nbackends; u++){ - if(backends[u].interval){ - res = backends[u].interval(); + for(u = 0; u < registry.n; u++){ + //only call interval if backend has instances + if(registry.instances[u] && registry.backends[u].interval){ + res = registry.backends[u].interval(); if((res / 1000) < secs){ + DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); secs = res / 1000; msecs = res % 1000; } else if(res / 1000 == secs && (res % 1000) < msecs){ + DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); msecs = res % 1000; } } @@ -245,14 +255,16 @@ struct timeval backend_timeout(){ MM_API int mm_backend_register(backend b){ if(!backend_match(b.name)){ - backends = realloc(backends, (nbackends + 1) * sizeof(backend)); - if(!backends){ + registry.backends = realloc(registry.backends, (registry.n + 1) * sizeof(backend)); + registry.instances = realloc(registry.instances, (registry.n + 1) * sizeof(instance**)); + if(!registry.backends || !registry.instances){ fprintf(stderr, "Failed to allocate memory\n"); - nbackends = 0; + registry.n = 0; return 1; } - backends[nbackends] = b; - nbackends++; + registry.backends[registry.n] = b; + registry.instances[registry.n] = NULL; + registry.n++; fprintf(stderr, "Registered backend %s\n", b.name); return 0; @@ -262,29 +274,25 @@ MM_API int mm_backend_register(backend b){ int backends_start(){ int rv = 0, current; - size_t n, u, p; instance** inst = NULL; + size_t n, u; - for(u = 0; u < nbackends; u++){ - //only start backends that have instances - for(p = 0; p < ninstances && instances[p]->backend != backends + u; p++){ - } - - //backend has no instances, skip the start call - if(p == ninstances){ + for(u = 0; u < registry.n; u++){ + //skip backends without instances + if(!registry.instances[u]){ continue; } //fetch list of instances - if(mm_backend_instances(backends[u].name, &n, &inst)){ - fprintf(stderr, "Failed to fetch instance list for initialization of backend %s\n", backends[u].name); + if(mm_backend_instances(registry.backends[u].name, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list for initialization of backend %s\n", registry.backends[u].name); return 1; } //start the backend - current = backends[u].start(n, inst); + current = registry.backends[u].start(n, inst); if(current){ - fprintf(stderr, "Failed to start backend %s\n", backends[u].name); + fprintf(stderr, "Failed to start backend %s\n", registry.backends[u].name); } //clean up @@ -295,24 +303,57 @@ int backends_start(){ return rv; } +static void channels_free(){ + size_t u, p; + for(u = 0; u < sizeof(channels.n) / sizeof(channels.n[0]); u++){ + DBGPF("Cleaning up channel registry bucket %" PRIsize_t " with %" PRIsize_t " channels", u, channels.n[u]); + for(p = 0; p < channels.n[u]; p++){ + DBGPF("Destroying channel %" PRIu64 " on instance %s\n", channels.entry[u][p]->ident, channels.entry[u][p]->instance->name); + //call the channel_free function if the backend supports it + if(channels.entry[u][p]->impl && channels.entry[u][p]->instance->backend->channel_free){ + channels.entry[u][p]->instance->backend->channel_free(channels.entry[u][p]); + } + free(channels.entry[u][p]); + } + free(channels.entry[u]); + channels.entry[u] = NULL; + channels.n[u] = 0; + } +} + int backends_stop(){ size_t u, n; instance** inst = NULL; - for(u = 0; u < nbackends; u++){ + //channels before instances to support proper shutdown procedures + channels_free(); + + //shut down the registry + for(u = 0; u < registry.n; u++){ //fetch list of instances - if(mm_backend_instances(backends[u].name, &n, &inst)){ - fprintf(stderr, "Failed to fetch instance list for shutdown of backend %s\n", backends[u].name); - n = 0; + if(mm_backend_instances(registry.backends[u].name, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list for shutdown of backend %s\n", registry.backends[u].name); inst = NULL; + n = 0; } - backends[u].shutdown(n, inst); + registry.backends[u].shutdown(n, inst); free(inst); inst = NULL; + + //free instances + for(inst = registry.instances[u]; inst && *inst; inst++){ + free((*inst)->name); + (*inst)->name = NULL; + (*inst)->backend = NULL; + free(*inst); + } + free(registry.instances[u]); + registry.instances[u] = NULL; } - free(backends); - nbackends = 0; + free(registry.backends); + free(registry.instances); + registry.n = 0; return 0; } @@ -8,9 +8,7 @@ instance* instance_match(char* name); struct timeval backend_timeout(); int backends_start(); int backends_stop(); -void instances_free(); -void channels_free(); -instance* mm_instance(); +instance* mm_instance(backend* b); /* Backend API */ MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create); diff --git a/backends/Makefile b/backends/Makefile index e31ff24..700c9b3 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 -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o @@ -46,6 +46,10 @@ maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) maweb.dll: LDLIBS += -lws2_32 maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL +rtpmidi.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +rtpmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +rtpmidi.dll: LDLIBS += -lws2_32 -liphlpapi + winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) winmidi.dll: LDLIBS += -lwinmm -lws2_32 @@ -61,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/artnet.c b/backends/artnet.c index 9fac332..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; } @@ -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; @@ -164,6 +170,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); @@ -192,13 +203,13 @@ 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){ - 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), @@ -213,22 +224,31 @@ 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)); + #ifdef _WIN32 + if(WSAGetLastError() != WSAEWOULDBLOCK){ + #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 + 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; } 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){ @@ -237,22 +257,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; } } @@ -268,12 +289,12 @@ 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; } - return artnet_transmit(inst); + return artnet_transmit(inst, artnet_fd[data->fd_index].output_instance + u); } return 0; @@ -304,16 +325,9 @@ 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 = mm_channel(inst, MAPPED_CHANNEL(data->data.map[p]), 0); - } - else{ - chan = mm_channel(inst, p, 0); - } - - if(!chan){ - LOGPF("Active channel %" PRIsize_t " on %s not known to core", p, inst->name); - return 1; + chan = data->data.channel + MAPPED_CHANNEL(data->data.map[p]); } if(IS_WIDE(data->data.map[p])){ @@ -361,7 +375,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); } } @@ -373,11 +387,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); @@ -400,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/artnet.h b/backends/artnet.h index d83999d..a517aa0 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 @@ -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/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/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..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; } @@ -549,38 +551,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/libmmbackend.c b/backends/libmmbackend.c index b9513ac..bad048c 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -1,6 +1,77 @@ #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; +} + +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); + #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; @@ -67,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, @@ -91,19 +162,24 @@ 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)); + 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(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)); } 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)); + 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){ - LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", strerror(errno)); + 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)); } } @@ -141,7 +217,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_socket_strerror(errno)); close(fd); return -1; } @@ -159,7 +235,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_socket_strerror(errno)); return 1; } total += sent; diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index aa0d0f0..1f0b4d7 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -5,6 +5,7 @@ #include <ws2tcpip.h> //#define close closesocket #else +#include <arpa/inet.h> #include <sys/socket.h> #include <netdb.h> #endif @@ -18,6 +19,34 @@ /*** BACKEND IMPLEMENTATION LIBRARY ***/ +/** 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 **/ /* @@ -43,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/lua.c b/backends/lua.c index e7ba9f9..98ce369 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -10,15 +10,18 @@ #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; 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; MM_PLUGIN_API int init(){ backend lua = { @@ -86,6 +89,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 +109,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,38 +136,131 @@ 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 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)); + return 0; + } + + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + + //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; const char* channel_name = NULL; - channel* channel = 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); + 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){ + 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_name[n])){ - channel = mm_channel(inst, n, 0); - if(!channel){ - return 0; + if(!strcmp(channel_name, data->channel[n].name)){ + data->channel[n].out = val.normalised; + if(!last_timestamp){ + data->channel[n].mark = 1; + } + else{ + mm_channel_event(mm_channel(inst, n, 0), val); } - mm_channel_event(channel, val); - data->output[n] = val.normalised; return 0; } } @@ -166,6 +269,31 @@ 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; + } + + 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 || current_handler == LUA_REFNIL){ + 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; @@ -176,10 +304,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){ @@ -243,28 +367,26 @@ 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); + 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++){ - 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; } } @@ -308,6 +430,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; @@ -322,6 +449,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); @@ -336,6 +464,9 @@ 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); + lua_register(data->interpreter, "cleanup_handler", lua_callback_cleanup_handler); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -352,26 +483,24 @@ 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]){ + //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"); return NULL; } @@ -382,26 +511,28 @@ 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; //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); } } + //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 @@ -412,7 +543,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 @@ -426,9 +558,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 @@ -449,29 +578,72 @@ 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; } +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, "cleanup_handler") + || !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; + channel_value v; //resolve channels to their handler functions for(u = 0; u < n; u++){ data = (lua_instance_data*) inst[u]->impl; + 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); + } + } + for(p = 0; p < data->channels; p++){ - //exclude reserved names - if(strcmp(data->channel_name[p], "output") - && strcmp(data->channel_name[p], "input_value") - && strcmp(data->channel_name[p], "output_value") - && strcmp(data->channel_name[p], "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; - } + data->channel[p].reference = default_handler; + 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); } } } @@ -482,9 +654,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; } @@ -494,16 +665,21 @@ 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 && data->cleanup_handler != LUA_REFNIL){ + 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 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); } @@ -511,6 +687,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 ebe2046..5587bf9 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -22,13 +22,21 @@ 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; + uint8_t mark; +} lua_channel_data; + typedef struct /*_lua_instance_data*/ { size_t channels; - char** channel_name; - int* reference; - double* input; - double* output; + lua_channel_data* channel; + lua_State* interpreter; + int cleanup_handler; + char* default_handler; } lua_instance_data; typedef struct /*_lua_interval_callback*/ { @@ -37,3 +45,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 db4cf39..b2f40e0 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -3,48 +3,87 @@ 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. +with the value (as a Lua `number` type) as parameter. Alternatively, a designated default channel handler +which will receive events for all incoming channels may be supplied in the configuration. + +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 | -| `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 | +| `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. 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 | | `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 | + +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 +-- 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) + 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) + 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); + output("narf", 0); + sleep(1000); + output("narf", 1.0); + 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) ``` 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. #### 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. @@ -59,20 +98,23 @@ 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. +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. 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/maweb.c b/backends/maweb.c index 6861d75..97d4cea 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 @@ -222,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"); @@ -340,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; } @@ -556,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++; } @@ -629,19 +643,30 @@ 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 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); + + 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); - data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0); if(data->fd < 0){ - return 1; + goto bail; } data->state = ws_new; @@ -654,15 +679,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 +723,19 @@ 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; + size_t start = data->next_host; + + do{ + if(!maweb_connect(inst)){ + break; + } + } while(data->next_host != start); + + return data->state != ws_closed ? 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; @@ -766,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; @@ -774,14 +817,12 @@ 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)); - //TODO close, reopen + LOGPF("Failed to receive on %s: %s", inst->name, mmbackend_socket_strerror(errno)); return 1; } else if(bytes_read == 0){ - //client closed connection - //TODO try to reopen - return 0; + //client closed connection, try to reopen the connection + return 1; } do{ @@ -801,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; } @@ -947,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); @@ -981,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 @@ -1003,8 +1060,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,10 +1074,10 @@ 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); - return 1; + //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); } } @@ -1027,15 +1089,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); @@ -1048,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; 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 diff --git a/backends/midi.c b/backends/midi.c index f73ebb4..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 { @@ -111,39 +109,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 +135,27 @@ 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, "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); @@ -224,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); @@ -236,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; @@ -253,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 108860e..d3d6e33 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -30,12 +30,9 @@ 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<channel>.<type><index>`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). -The earlier syntax of `<type><channel>.<index>` is officially deprecated but still supported for compatibility -reasons. This support may be removed at some future time. The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`. @@ -59,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 <portname>`. diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 168e077..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; } @@ -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; @@ -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 754c290..5887a50 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) @@ -231,7 +232,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 +332,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] @@ -524,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; @@ -666,7 +667,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); @@ -695,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; } @@ -703,16 +704,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; @@ -724,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; } @@ -747,7 +744,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++){ @@ -761,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; @@ -813,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; @@ -845,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 @@ -876,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/python.c b/backends/python.c index 70c2548..bd73a20 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; } } @@ -115,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} }; @@ -126,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; } } @@ -254,6 +257,35 @@ 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){ + 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); + } + + if(!current_handler){ + Py_INCREF(Py_None); + return Py_None; + } + + //do not decrease refcount on current_handler here as the reference may be used by python code again + 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; @@ -261,12 +293,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; } @@ -378,6 +408,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; @@ -390,13 +425,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} }; @@ -523,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 @@ -561,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); @@ -569,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); } } } @@ -600,38 +637,62 @@ 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; + channel_value v; //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); + } + //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 @@ -642,28 +703,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); - //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? @@ -671,25 +748,18 @@ static int python_shutdown(size_t n, instance** inst){ Py_XDECREF(data->socket[p].handler); } - //release interval references - for(p = 0; p <intervals; p++){ - Py_XDECREF(interval[p].reference); - } - + //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 8ca12f9..539389b 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*/ { @@ -41,4 +42,8 @@ typedef struct /*_python_instance_data*/ { size_t channels; mmpython_channel* channel; mmpython_channel* current_channel; + + char* default_handler; + PyObject* handler; + PyObject* cleanup_handler; } python_instance_data; diff --git a/backends/python.md b/backends/python.md index f06e504..a78d972 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. @@ -16,13 +17,17 @@ 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. | + +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 @@ -47,12 +52,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. @@ -67,9 +76,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). @@ -90,6 +100,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` diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c new file mode 100644 index 0000000..7c5aa69 --- /dev/null +++ b/backends/rtpmidi.c @@ -0,0 +1,1790 @@ +#define BACKEND_NAME "rtpmidi" +//#define DEBUG + +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <ctype.h> + +//mmbackend pulls in windows.h, required before more specific includes +#include "libmmbackend.h" +#include "rtpmidi.h" + +#ifdef _WIN32 +#include <iphlpapi.h> +#else +#include <arpa/inet.h> +#include <net/if.h> +#include <sys/types.h> +#include <ifaddrs.h> +#endif + +//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 + +/* + * 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*/ { + //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; + #ifdef _WIN32 + unsigned mdns_adapter; + unsigned mdns6_adapter; + #endif + + uint8_t detect; + uint64_t last_service; + + size_t addresses; + rtpmidi_addr* address; + + size_t invites; + rtpmidi_invite* invite; +} cfg = { + .mdns_fd = -1, + .mdns4_fd = -1, + .mdns_name = NULL, + .mdns_interface = NULL, + + .detect = 0, + .last_service = 0, + + .addresses = 0, + .address = NULL, + + .invites = 0, + .invite = NULL +}; + +MM_PLUGIN_API int init(){ + backend rtpmidi = { + .name = BACKEND_NAME, + .conf = rtpmidi_configure, + .create = rtpmidi_instance, + .conf_instance = rtpmidi_configure_instance, + .channel = rtpmidi_channel, + .handle = rtpmidi_set, + .interval = rtpmidi_interval, + .process = rtpmidi_handle, + .start = rtpmidi_start, + .shutdown = rtpmidi_shutdown + }; + + if(sizeof(rtpmidi_channel_ident) != sizeof(uint64_t)){ + LOG("Channel identification union out of bounds"); + return 1; + } + + if(mm_backend_register(rtpmidi)){ + LOG("Failed to register backend"); + return 1; + } + + return 0; +} + +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; + + //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; + } + + //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)); + if(!out->name){ + LOG("Failed to allocate memory"); + return 1; + } + 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; +} + +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){ + 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 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 int rtpmidi_announce_addrs(){ + char repr[INET6_ADDRSTRLEN + 1] = "", iface[2048] = ""; + union { + struct sockaddr_in* in4; + struct sockaddr_in6* in6; + struct sockaddr* in; + } addr; + + #ifdef _WIN32 + IP_ADAPTER_UNICAST_ADDRESS_LH* unicast_addr = NULL; + IP_ADAPTER_ADDRESSES addrs[250] , *iter = NULL; + size_t bytes_alloc = sizeof(addrs); + + 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... + 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; + } + + 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 && strncmp(iface, cfg.mdns_interface, min(strlen(iface), strlen(cfg.mdns_interface)))){ + 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 + struct ifaddrs* ifa = NULL, *iter = NULL; + + if(getifaddrs(&ifa)){ + LOGPF("Failed to get adapter address information: %s", mmbackend_socket_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){ + 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; + } + + 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)), iface); + cfg.addresses++; + } + } + + #ifndef _WIN32 + 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, (int64_t) RTPMIDI_SERVICE_INTERVAL - (int64_t) (mm_timestamp() - cfg.last_service)); +} + +static int rtpmidi_configure(char* option, char* value){ + if(!strcmp(option, "mdns-name")){ + if(cfg.mdns_name){ + LOG("Duplicate mdns-name assignment"); + return 1; + } + + 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 mmbackend_strdup(&cfg.mdns_interface, value); + } + else if(!strcmp(option, "detect")){ + cfg.detect = 0; + if(!strcmp(value, "on")){ + cfg.detect = 1; + } + return 0; + } + + LOGPF("Unknown backend configuration option %s", option); + return 1; +} + +static int rtpmidi_bind_instance(instance* inst, 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, 1); + if(data->fd < 0){ + return 1; + } + + if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ + LOGPF("Failed to fetch data port information: %s", mmbackend_socket_strerror(errno)); + return 1; + } + + //bind control port + 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, 1); + if(data->control_fd < 0){ + LOGPF("Failed to bind control port %s for instance %s", control_port, inst->name); + return 1; + } + + 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); + LOGPF("Direct mode instance %s listening on port %d", inst->name, data->control_port); + } + + 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* 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)){ + //if yes, update connection flag (but not learned flag because that doesn't change) + data->peer[u].connected = connected; + return 0; + } + + if(!data->peer[u].active){ + p = u; + } + } + + 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->peer[p].active = 1; + data->peer[p].learned = learned; + data->peer[p].connected = connected; + data->peer[p].invite = invite_reference; + memcpy(&(data->peer[p].dest), sock_addr, sock_len); + data->peer[p].dest_len = sock_len; + return 0; +} + +static int rtpmidi_push_invite(instance* inst, char* peer){ + size_t u, p; + + //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 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.invites = 0; + return 1; + } + + cfg.invite[u].inst = inst; + cfg.invite[u].invites = 0; + cfg.invite[u].name = NULL; + + cfg.invites++; + } + + //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.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.invite[u].invites = 0; + return 1; + } + + //append the new invitee + cfg.invite[u].name[p] = strdup(peer); + if(!cfg.invite[u].name[p]){ + LOG("Failed to allocate memory"); + return 1; + } + + cfg.invite[u].invites++; + return 0; +} + +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; + cmd->command = htobe16(command); + cmd->version = htobe32(2); + cmd->token = token ? token : (((uint32_t) rand()) << 16 | rand()); + cmd->ssrc = htobe32(data->ssrc); + + //append session name to packet + 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 + 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: %s", inst->name, mmbackend_socket_strerror(errno)); + return 1; + } + return 0; +} + +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; + + 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 = 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, token); +} + +static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + char* host = NULL, *port = NULL; + struct sockaddr_storage sock_addr; + socklen_t sock_len = sizeof(sock_addr); + + if(!strcmp(option, "mode")){ + if(!strcmp(value, "direct")){ + data->mode = direct; + return 0; + } + else if(!strcmp(value, "apple")){ + data->mode = apple; + return 0; + } + 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){ + LOGPF("Random SSRC will be generated for instance %s", inst->name); + } + return 0; + } + else if(!strcmp(option, "bind")){ + if(data->mode == unconfigured){ + LOGPF("Please specify mode for instance %s before setting bind host", inst->name); + return 1; + } + + mmbackend_parse_hostspec(value, &host, &port, NULL); + + if(!host){ + LOGPF("Could not parse bind host specification %s for instance %s", value, inst->name); + return 1; + } + + return rtpmidi_bind_instance(inst, data, host, port); + } + else if(!strcmp(option, "learn")){ + if(data->mode != direct){ + LOG("'learn' option is only valid for direct mode instances"); + return 1; + } + data->learn_peers = 0; + if(!strcmp(value, "true")){ + data->learn_peers = 1; + } + 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); + return 1; + } + + if(mmbackend_parse_sockaddr(host, port, &sock_addr, &sock_len)){ + LOGPF("Failed to resolve peer %s on instance %s", value, inst->name); + return 1; + } + + //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 = htobe16(be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) + 1); + } + + return rtpmidi_push_peer(data, (struct sockaddr*) &sock_addr, sock_len, 0, 0, -1); + } + else if(!strcmp(option, "invite")){ + if(data->mode != apple){ + LOG("'invite' option is only valid for apple mode instances"); + return 1; + } + + return rtpmidi_push_invite(inst, value); + } + else if(!strcmp(option, "join")){ + if(data->mode != apple){ + LOG("'join' option is only valid for apple mode instances"); + return 1; + } + return mmbackend_strdup(&data->accept, value); + } + + LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); + return 1; +} + +static int rtpmidi_instance(instance* inst){ + rtpmidi_instance_data* data = calloc(1, sizeof(rtpmidi_instance_data)); + if(!data){ + LOG("Failed to allocate memory"); + return 1; + } + data->fd = -1; + data->control_fd = -1; + + inst->impl = data; + return 0; +} + +static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ + char* next_token = spec; + rtpmidi_channel_ident ident = { + .label = 0 + }; + + if(!strncmp(spec, "ch", 2)){ + next_token += 2; + if(!strncmp(spec, "channel", 7)){ + next_token = spec + 7; + } + } + else{ + LOGPF("Invalid channel specification %s", spec); + return NULL; + } + + ident.fields.channel = strtoul(next_token, &next_token, 10); + if(ident.fields.channel > 15){ + LOGPF("Channel out of range in channel spec %s", spec); + return NULL; + } + + if(*next_token != '.'){ + LOGPF("Channel specification %s does not conform to channel<X>.<control><Y>", 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{ + LOGPF("Unknown control type in spec %s", spec); + return NULL; + } + + ident.fields.control = strtoul(next_token, NULL, 10); + + if(ident.label){ + return mm_channel(inst, ident.label, 1); + } + return NULL; +} + +static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + rtpmidi_header* rtp_header = (rtpmidi_header*) frame; + rtpmidi_command_header* command_header = (rtpmidi_command_header*) (frame + sizeof(rtpmidi_header)); + size_t offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0; + uint8_t* payload = frame + offset; + rtpmidi_channel_ident ident; + + rtp_header->vpxcc = RTPMIDI_HEADER_MAGIC; + //some receivers seem to have problems reading rfcs and interpreting the marker bit correctly + rtp_header->mpt = (data->mode == apple ? 0 : 0x80) | RTPMIDI_HEADER_TYPE; + rtp_header->sequence = htobe16(data->sequence++); + rtp_header->timestamp = mm_timestamp() * 10; //just assume 100msec resolution because rfc4695 handwaves it + rtp_header->ssrc = htobe32(data->ssrc); + + //midi command section header + //TODO enable the journal bit here + command_header->flags = 0xA0; //extended length header, first entry in list has dtime + + //midi list + for(u = 0; u < num; u++){ + ident.label = c[u]->ident; + + //encode timestamp + payload[0] = 0; + + //encode midi command + payload[1] = ident.fields.type | ident.fields.channel; + payload[2] = ident.fields.control; + payload[3] = v[u].normalised * 127.0; + + if(ident.fields.type == pitchbend){ + payload[2] = ((int)(v[u].normalised * 16384.0)) & 0x7F; + payload[3] = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; + } + //channel-wide aftertouch is only 2 bytes + else if(ident.fields.type == aftertouch){ + payload[2] = payload[3]; + payload -= 1; + offset -= 1; + } + + payload += 4; + offset += 4; + } + + //update command section length + //FIXME this might overrun, might check the number of events at some point + command_header->flags |= (((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0x0F00) >> 8); + command_header->length = ((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0xFF); + + //TODO journal section + + for(u = 0; u < data->peers; u++){ + 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); + } + } + + return 0; +} + +static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t response[RTPMIDI_PACKET_BUFFER] = ""; + apple_command* command = (apple_command*) frame; + char* session_name = (char*) frame + sizeof(apple_command); + size_t n, u; + + 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; + } + + 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 + 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, (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"); + 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, (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 = htobe16(be16toh(((struct sockaddr_in*) peer)->sin_port) + 1); + //send invite + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, 0, apple_invite, 0); + } + return 0; + } + 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... + ((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)){ + 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 + 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; + } + + 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){ + //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); + } + + 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 (and ignore) 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); + } + } + + //push event + 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){ + 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){ + LOGPF("Failed to receive for instance %s", inst->name); + return 1; + } + + if(bytes_recv < sizeof(rtpmidi_header)){ + LOGPF("Skipping short packet on instance %s", inst->name); + return 0; + } + + //FIXME might want to filter data input from sources that are not registered peers + if(data->mode == apple && rtp_header->vpxcc == 0xFF && rtp_header->mpt == 0xFF){ + return rtpmidi_handle_applemidi(inst, data->fd, frame, bytes_recv, &sock_addr, sock_len); + } + else if(rtp_header->vpxcc != RTPMIDI_HEADER_MAGIC || RTPMIDI_GET_TYPE(rtp_header->mpt) != RTPMIDI_HEADER_TYPE){ + LOGPF("Frame with invalid header magic on %s", inst->name); + return 0; + } + + //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){ + for(u = 0; u < data->peers; u++){ + if(data->peer[u].active + && data->peer[u].dest_len == sock_len + && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + break; + } + } + + if(u == data->peers){ + LOGPF("Learned new peer on %s", inst->name); + return rtpmidi_push_peer(data, (struct sockaddr*) &sock_addr, sock_len, 1, 1, -1); + } + } + return 0; +} + +static int rtpmidi_handle_control(instance* inst){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + struct sockaddr_storage sock_addr; + socklen_t sock_len = sizeof(sock_addr); + ssize_t bytes_recv = recvfrom(data->control_fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); + + if(bytes_recv < 0){ + 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)){ + LOGPF("Skipping short packet on control socket of instance %s", 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); + } + + LOGPF("Unknown session protocol frame received on instance %s", inst->name); + return 0; +} + +static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){ + 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} + }; + + //send to ipv4 and ipv6 mcasts + sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); + sendto(cfg.mdns4_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)); + return 0; +} + +static int rtpmidi_mdns_detach(instance* inst){ + 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(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(inst->name) + 2); + + free(name.name); + return rtpmidi_mdns_broadcast(frame, offset); +bail: + free(name.name); + return 1; +} + +//FIXME this should not exceed 1500 bytes +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; + dns_rr_srv* srv = NULL; + dns_name name = { + .alloc = 0 + }; + size_t offset = 0, host_offset = 0, u = 0; + ssize_t bytes = 0; + + hdr->id = 0; + hdr->flags[0] = 0x84; + hdr->flags[1] = 0; + hdr->questions = hdr->servers = 0; + hdr->answers = htobe16(4); + hdr->additional = htobe16(cfg.addresses); + offset = sizeof(dns_header); + + //answer 1: SRV FQDN + 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; + } + offset += bytes; + + srv = (dns_rr_srv*) (frame + offset); + srv->priority = 0; + srv->weight = 0; + 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 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); + goto bail; + } + 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); + + 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); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 12, 1, 4500, 2); + if(bytes < 0){ + goto bail; + } + offset += bytes; + + //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; + + 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; + frame[offset++] = sizeof(dns_header); + + //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, + (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; + + 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); + return rtpmidi_mdns_broadcast(frame, offset); +bail: + free(name.name); + return 1; +} + +static int rtpmidi_service(){ + size_t n, u, p; + instance** inst = NULL; + rtpmidi_instance_data* data = NULL; + 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 + } + }; + + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + LOG("Failed to fetch instances"); + 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 || cfg.mdns4_fd >= 0) + && (!data->last_announce || mm_timestamp() - data->last_announce > RTPMIDI_ANNOUNCE_INTERVAL)){ + rtpmidi_mdns_announce(inst[u]); + } + + 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 = htobe16(be16toh(((struct sockaddr_in*) &control_peer)->sin_port) - 1); + + 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 + DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p); + rtpmidi_peer_applecommand(inst[u], p, 1, apple_invite, 0); + } + } + } + } + + free(inst); + 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, 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, socklen_t source_len){ + 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)){ + 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 (%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); + } + + + return 0; +} + +static int rtpmidi_handle_mdns(int fd){ + uint8_t buffer[RTPMIDI_PACKET_BUFFER]; + dns_header* hdr = (dns_header*) buffer; + dns_name name = { + .alloc = 0 + }, host = name; + 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; + bytes = recvfrom(fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len)){ + if(bytes < sizeof(dns_header)){ + continue; + } + + //decode basic header + 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); + + //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, 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, + 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); + } + + free(name.name); + free(host.name); + #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; +} + +static int rtpmidi_handle(size_t num, managed_fd* fds){ + size_t u; + int rv = 0; + instance* inst = NULL; + rtpmidi_instance_data* data = NULL; + + //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); + if(rtpmidi_service()){ + return 1; + } + cfg.last_service = mm_timestamp(); + } + + for(u = 0; u < num; u++){ + if(!fds[u].impl){ + //handle mDNS discovery input + rtpmidi_handle_mdns(fds[u].fd); + } + else{ + //handle rtp/control input + inst = (instance*) fds[u].impl; + data = (rtpmidi_instance_data*) inst->impl; + if(fds[u].fd == data->fd){ + rv |= rtpmidi_handle_data(inst); + } + else if(fds[u].fd == data->control_fd){ + rv |= rtpmidi_handle_control(inst); + } + else{ + LOG("Signaled for unknown descriptor"); + } + } + } + + return rv; +} + +static int rtpmidi_start_mdns(){ + //use ip_mreqn where possible, but that renames the interface member + #ifdef _WIN32 + struct ip_mreq mcast_req = { + .imr_interface.s_addr = INADDR_ANY, + #else + struct ip_mreqn mcast_req = { + .imr_address.s_addr = INADDR_ANY, + #endif + .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xe00000fb)) + }; + + 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; + } + + 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); + if(cfg.mdns_fd < 0 && cfg.mdns4_fd < 0){ + LOG("Failed to create requested mDNS descriptors"); + return 1; + } + + //join ipv4 multicast group + 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(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) | mm_manage_fd(cfg.mdns4_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_requested = 0; + + 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){ + LOGPF("Instance %s is missing a mode configuration", inst[u]->name); + return 1; + } + + //generate random ssrc's + if(!data->ssrc){ + data->ssrc = ((uint32_t) rand()) << 16 | rand(); + } + + //if not bound, bind to default + 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; + } + + //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; + } + } + else if(data->mode == apple){ + mdns_requested = 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); + return 1; + } + fds += (data->control_fd >= 0) ? 2 : 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_requested){ + fds += 2; + } + + LOGPF("Registered %" PRIsize_t " descriptors to core", fds); + return 0; +} + +static int rtpmidi_shutdown(size_t n, instance** inst){ + rtpmidi_instance_data* data = NULL; + size_t u, p; + + for(u = 0; u < n; u++){ + data = (rtpmidi_instance_data*) inst[u]->impl; + + if((cfg.mdns_fd >= 0 || cfg.mdns4_fd >= 0) && data->mode == apple){ + rtpmidi_mdns_detach(inst[u]); + } + + if(data->fd >= 0){ + close(data->fd); + } + + if(data->control_fd >= 0){ + close(data->control_fd); + } + + free(data->accept); + data->accept = NULL; + + free(data->peer); + data->peer = NULL; + data->peers = 0; + + free(inst[u]->impl); + inst[u]->impl = NULL; + } + + for(u = 0; u < cfg.invites; u++){ + for(p = 0; p < cfg.invite[u].invites; p++){ + free(cfg.invite[u].name[p]); + } + free(cfg.invite[u].name); + } + free(cfg.invite); + cfg.invite = NULL; + cfg.invites = 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); + } + 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 new file mode 100644 index 0000000..7e6eccc --- /dev/null +++ b/backends/rtpmidi.h @@ -0,0 +1,182 @@ +#ifndef _WIN32 +#include <sys/socket.h> +#endif +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static int rtpmidi_configure(char* option, char* value); +static int rtpmidi_configure_instance(instance* instance, char* option, char* value); +static 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); +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 +#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_ANNOUNCE_INTERVAL (60 * 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, + cc = 0xB0, + pressure = 0xA0, + aftertouch = 0xD0, + pitchbend = 0xE0 +}; + +typedef enum /*_rtpmidi_instance_mode*/ { + unconfigured = 0, + direct, + apple +} rtpmidi_instance_mode; + +typedef union { + struct { + uint8_t pad[5]; + uint8_t type; + uint8_t channel; + uint8_t control; + } fields; + uint64_t label; +} rtpmidi_channel_ident; + +typedef struct /*_rtpmidi_peer*/ { + struct sockaddr_storage dest; + socklen_t dest_len; + //uint32_t ssrc; + 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 (used to track ipv6/ipv4 overlapping invitations) +} rtpmidi_peer; + +typedef struct /*_rtmidi_instance_data*/ { + rtpmidi_instance_mode mode; + + int fd; + int control_fd; + uint16_t control_port; /*convenience member set by rtpmidi_bind_instance*/ + + size_t peers; + rtpmidi_peer* peer; + uint32_t ssrc; + uint16_t sequence; + + //apple-midi config + char* accept; + uint64_t last_announce; + + //direct mode config + uint8_t learn_peers; +} rtpmidi_instance_data; + +typedef struct /*rtpmidi_invited_peer*/ { + instance* inst; + size_t invites; + char** name; +} rtpmidi_invite; + +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; + +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; + char* name; + size_t length; +} dns_name; + +#pragma pack(push, 1) +typedef struct /*_apple_session_command*/ { + uint16_t res1; + uint16_t command; + uint32_t version; + uint32_t token; + uint32_t ssrc; + //char* name +} apple_command; + +typedef struct /*_apple_session_sync*/ { + uint16_t res1; + uint16_t command; + uint32_t ssrc; + uint8_t count; + uint8_t res2[3]; + uint64_t timestamp[3]; +} apple_sync_frame; + +typedef struct /*_apple_session_feedback*/ { + uint16_t res1; + uint8_t command[2]; + uint32_t ssrc; + uint32_t sequence; +} apple_journal_feedback; + +typedef struct /*_rtp_midi_header*/ { + uint8_t vpxcc; + uint8_t mpt; + uint16_t sequence; + uint32_t timestamp; + uint32_t ssrc; +} rtpmidi_header; + +typedef struct /*_rtp_midi_command*/ { + uint8_t flags; + uint8_t length; +} rtpmidi_command_header; + +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; + +typedef struct /*_dns_rr_srv*/ { + uint16_t priority; + uint16_t weight; + uint16_t port; +} dns_rr_srv; +#pragma pack(pop) diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md new file mode 100644 index 0000000..82548bf --- /dev/null +++ b/backends/rtpmidi.md @@ -0,0 +1,97 @@ +### The `rtpmidi` backend + +This backend provides read-write access to RTP MIDI streams, which transfer MIDI data +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. +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: The instance will be able to communicate (either as participant + 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. + +#### 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 (`<mdns-name>.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. If this name matches an interface exactly, discovery uses exactly this device. | + +#### Instance configuration + +Common instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `ssrc` | `0xDEADBEEF` | Randomly generated | 32-bit synchronization source identifier | +| `mode` | `direct` | none | Instance session management mode (`direct` or `apple`) | +| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times. Bypasses session discovery (but still performs session negotiation) | + +`direct` mode instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `bind` | `10.1.2.1 9001` | `:: <random>` | Local network address to bind to | +| `learn` | `true` | `false` | Accept new peers for data exchange at runtime | + +`apple` mode instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `bind` | `10.1.2.1 9001` | `:: <random>` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated). | +| `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 + +The `rtpmidi` backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are + +* `cc` - Control Changes +* `note` - Note On/Off messages +* `pressure` - Note pressure/aftertouch messages +* `aftertouch` - Channel-wide aftertouch messages +* `pitch` - Channel pitchbend messages + +A MIDIMonster channel is specified using the syntax `channel<channel>.<type><index>`. The shorthand `ch` may be +used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). + +The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`. + +MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which +additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called +'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. + +Example mappings: + +``` +rmidi1.ch0.note9 > rmidi2.channel1.cc4 +rmidi1.channel15.pressure1 > rmidi1.channel0.note0 +rmidi1.ch1.aftertouch > rmidi2.ch2.cc0 +rmidi1.ch0.pitch > rmidi2.ch1.pitch +``` + +#### Known bugs / problems + +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 +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 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. diff --git a/backends/sacn.c b/backends/sacn.c index 79ffb46..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; } @@ -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)); } } @@ -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; } @@ -231,6 +239,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); @@ -259,12 +272,13 @@ 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){ - 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), @@ -299,16 +313,26 @@ 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)); - } + #ifdef _WIN32 + if(WSAGetLastError() != WSAEWOULDBLOCK){ + #else + if(errno != EAGAIN){ + #endif + LOGPF("Failed to output frame for instance %s: %s", inst->name, mmbackend_socket_strerror(errno)); + 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; } @@ -317,10 +341,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; @@ -348,26 +368,26 @@ 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 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; } } - sacn_transmit(inst); + sacn_transmit(inst, global_cfg.fd[data->fd_index].universe + u); } return 0; @@ -418,16 +438,9 @@ 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 = mm_channel(inst, MAPPED_CHANNEL(inst_data->data.map[u]), 0); - } - else{ - chan = mm_channel(inst, u, 0); - } - - if(!chan){ - LOGPF("Active channel %" PRIsize_t " on %s not known to core", u, inst->name); - return 1; + chan = inst_data->data.channel + MAPPED_CHANNEL(inst_data->data.map[u]); } //generate value @@ -494,7 +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){ - LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, strerror(errno)); + #ifdef _WIN32 + if(WSAGetLastError() != WSAEWOULDBLOCK){ + #else + if(errno != EAGAIN){ + #endif + LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, mmbackend_socket_strerror(errno)); + } } } } @@ -535,7 +554,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); } } @@ -548,11 +567,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); @@ -578,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){ @@ -630,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)); } } diff --git a/backends/sacn.h b/backends/sacn.h index 4642e59..4138f45 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" @@ -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*/ { diff --git a/backends/winmidi.c b/backends/winmidi.c index ad9b02d..030062d 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; @@ -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; @@ -213,7 +208,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 +305,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 +330,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 +429,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, 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 +467,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 +476,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; @@ -2,6 +2,11 @@ #include <ctype.h> #include <unistd.h> #include <errno.h> +#ifndef _WIN32 +#include <limits.h> +#endif + +#define BACKEND_NAME "core/cfg" #include "midimonster.h" #include "config.h" #include "backend.h" @@ -349,6 +354,10 @@ static int config_line(char* line){ } } } + else if(!strncmp(line, "[include ", 9)){ + line[strlen(line) - 1] = 0; + return config_read(line + 9); + } else if(!strcmp(line, "[map]")){ //mapping configuration parser_state = map; @@ -388,7 +397,7 @@ static int config_line(char* line){ return 1; } - current_instance = mm_instance(); + current_instance = mm_instance(current_backend); if(!current_instance){ return 1; } @@ -493,7 +502,7 @@ int config_read(char* cfg_filepath){ char* line_raw = NULL; //create heap copy of file name because original might be in readonly memory - char* source_dir = strdup(cfg_filepath), *source_file = NULL; + char* source_dir = strdup(cfg_filepath), *source_file = NULL, original_dir[PATH_MAX * 2] = ""; #ifdef _WIN32 char path_separator = '\\'; #else @@ -510,6 +519,12 @@ int config_read(char* cfg_filepath){ if(source_file){ *source_file = 0; source_file++; + + if(!getcwd(original_dir, sizeof(original_dir))){ + fprintf(stderr, "Failed to read current working directory: %s\n", strerror(errno)); + goto bail; + } + if(chdir(source_dir)){ fprintf(stderr, "Failed to change to configuration file directory %s: %s\n", source_dir, strerror(errno)); goto bail; @@ -519,10 +534,11 @@ int config_read(char* cfg_filepath){ source_file = source_dir; } + fprintf(stderr, "Reading configuration file %s\n", cfg_filepath); source = fopen(source_file, "r"); if(!source){ - fprintf(stderr, "Failed to open configuration file for reading\n"); + fprintf(stderr, "Failed to open %s for reading\n", cfg_filepath); goto bail; } @@ -536,6 +552,11 @@ int config_read(char* cfg_filepath){ rv = 0; bail: + //change back to previous directory to allow recursive configuration file parsing + if(source_file && source_dir != source_file){ + chdir(original_dir); + } + free(source_dir); if(source){ fclose(source); diff --git a/configs/launchctl-sacn.cfg b/configs/launchctl-sacn.cfg index dedfc0f..10a736a 100644 --- a/configs/launchctl-sacn.cfg +++ b/configs/launchctl-sacn.cfg @@ -7,7 +7,7 @@ name = MIDIMonster [backend sacn] -bind = 0.0.0.0 +bind = 0.0.0.0 5568 local [midi lc] read = Launch Control diff --git a/configs/layering.cfg b/configs/layering.cfg index 7adcd6f..f312a65 100644 --- a/configs/layering.cfg +++ b/configs/layering.cfg @@ -15,9 +15,10 @@ universe = 1 [lua layers] script = layering.lua +default-handler = handler [map] -in.ch0.cc{0..15} > layers.in{0..15} +in.ch1.cc{0..15} > layers.{0..15} layers.out{0..511} > out.{1..512} in.ch0.note0 > layers.control diff --git a/configs/layering.lua b/configs/layering.lua index 5d9458d..5ddde65 100644 --- a/configs/layering.lua +++ b/configs/layering.lua @@ -3,74 +3,14 @@ -- so we have 32 possible offsets (32 * 16 = 512) current_layer = 0 --- Set the current_layer based on the control input channel -function control(value) - current_layer = math.floor(value * 31.99); -end - --- Handler functions for the input channels --- Calculate the channel offset and just output the value the input channel provides -function in0(value) - output("out"..((current_layer * 16)), value) - print("Output on out"..(current_layer * 16)) -end - -function in1(value) - output("out"..((current_layer * 16) + 1), value) -end - -function in2(value) - output("out"..((current_layer * 16) + 2), value) -end - -function in3(value) - output("out"..((current_layer * 16) + 3), value) -end - -function in4(value) - output("out"..((current_layer * 16) + 4), value) -end - -function in5(value) - output("out"..((current_layer * 16) + 5), value) -end - -function in6(value) - output("out"..((current_layer * 16) + 6), value) -end - -function in7(value) - output("out"..((current_layer * 16) + 7), value) -end - -function in8(value) - output("out"..((current_layer * 16) + 8), value) -end - -function in9(value) - output("out"..((current_layer * 16) + 9), value) -end - -function in10(value) - output("out"..((current_layer * 16) + 10), value) -end - -function in11(value) - output("out"..((current_layer * 16) + 11), value) -end - -function in12(value) - output("out"..((current_layer * 16) + 12), value) -end - -function in13(value) - output("out"..((current_layer * 16) + 13), value) -end - -function in14(value) - output("out"..((current_layer * 16) + 14), value) -end - -function in15(value) - output("out"..((current_layer * 16) + 15), value) -end +function handler(value) + if(input_channel() == "control") then + -- Set the current_layer based on the control input channel + current_layer = math.floor(value * 31.99); + else + -- Handler functions for the input channels + -- Calculate the channel offset and just output the value the input channel provides + output("out"..((current_layer * 16) + tonumber(input_channel())), value) + print("Output on out"..(current_layer * 16)) + end +end
\ No newline at end of file diff --git a/configs/osc-artnet.cfg b/configs/osc-artnet.cfg index ab1d767..35b2111 100644 --- a/configs/osc-artnet.cfg +++ b/configs/osc-artnet.cfg @@ -5,7 +5,7 @@ bind = 0.0.0.0 [osc touch] -bind = * 8000 +bind = 0.0.0.0 8000 dest = learn@8001 [artnet out] diff --git a/configs/print.lua b/configs/print.lua new file mode 100644 index 0000000..dc28bb6 --- /dev/null +++ b/configs/print.lua @@ -0,0 +1,5 @@ +-- This function prints the name of the channel it handles and its current value +-- It can be used for a simple debug output with the `default-handler` configuration option +function printchannel(value) + print(input_channel() .. " @ " .. value) +end diff --git a/configs/rtpmidi.cfg b/configs/rtpmidi.cfg new file mode 100644 index 0000000..3223bd7 --- /dev/null +++ b/configs/rtpmidi.cfg @@ -0,0 +1,22 @@ +; Simple RTP MIDI example configuration + +[backend rtpmidi] +; This causes the backend itself to print channel values as they come in +detect = on +; When connecting multiple MIDIMonster hosts via RTP MIDI, set this to something different on each computer +mdns-name = midimonster-host + +[rtpmidi rtp] +mode = apple +; Invite everyone we see on the network +invite = * + +; This instance just sends all incoming events to the `printchannel` function +[lua print] +script = print.lua +default-handler = printchannel + +; Map all notes and CC's coming in to the Lua instance +[map] +rtp.ch{0..15}.cc{0..127} > print.ch{0..15}.cc{0..127} +rtp.ch{0..15}.note{0..127} > print.ch{0..15}.cnote{0..127} diff --git a/installer.sh b/installer.sh index 15ad203..66eef99 100755 --- a/installer.sh +++ b/installer.sh @@ -1,213 +1,280 @@ #!/bin/bash ################################################ SETUP ################################################ -deps=(libasound2-dev libevdev-dev liblua5.3-dev libjack-jackd2-dev pkg-config libssl-dev gcc make wget git) -user=$(whoami) # for bypassing user check replace "$(whoami)" with "root". - -tmp_path=$(mktemp -d) # Repo download path -updater_dir=/etc/midimonster-updater # Updater download + config path -updater_file=$updater_dir/updater.conf - -latest_version=$(curl --silent "https://api.github.com/repos/cbdevnet/midimonster/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') - -makeargs=all # Build args - -VAR_DESTDIR="" # Unused -VAR_PREFIX="/usr" -VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" -VAR_DEFAULT_CFG="/etc/midimonster/midimonster.cfg" -VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" - -bold=$(tput bold) -normal=$(tput sgr0) - -################################################ SETUP ################################################ +deps=( + libasound2-dev + libevdev-dev + liblua5.3-dev + libjack-jackd2-dev + pkg-config + libssl-dev + python3-dev + gcc + make + wget + git +) +# Replace this with 'root' to bypass the user check +user="$(whoami)" +# Temporary directory used for repository clone +tmp_path="$(mktemp -d)" +# Installer/updater install directory +updater_dir="/etc/midimonster-updater" + +latest_version="$(curl --silent "https://api.github.com/repos/cbdevnet/midimonster/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')" + +# make invocation arguments +makeargs="all" + +normal="$(tput sgr0)" +dim="$(tput dim)" +bold="$(tput bold)" +uline="$(tput smul)" +c_red="$(tput setaf 1)" +c_green="$(tput setaf 2)" +c_mag="$(tput setaf 5)" + +DEFAULT_PREFIX="/usr" +DEFAULT_PLUGINPATH="/lib/midimonster" +DEFAULT_CFGPATH="/etc/midimonster/midimonster.cfg" +DEFAULT_EXAMPLES="/share/midimonster" ############################################## FUNCTIONS ############################################## - -INSTALL-DEPS () { ##Install deps from array "$deps" - for t in ${deps[@]}; do - if [ $(dpkg-query -W -f='${Status}' $t 2>/dev/null | grep -c "ok installed") -eq 0 ]; then - printf "Installing %s\n" "$t" - apt-get install $t; - printf "Done\n"; - else - printf "%s already installed!\n" "$t" - fi - done - printf "\n" +assign_defaults(){ + VAR_PREFIX="${VAR_PREFIX:-$DEFAULT_PREFIX}" + VAR_PLUGINS="${VAR_PLUGINS:-$VAR_PREFIX$DEFAULT_PLUGINPATH}" + VAR_DEFAULT_CFG="${VAR_DEFAULT_CFG:-$DEFAULT_CFGPATH}" + VAR_EXAMPLE_CFGS="${VAR_EXAMPLE_CFGS:-$VAR_PREFIX$DEFAULT_EXAMPLES}" } -NIGHTLY_CHECK () { - #Asks for nightly version - read -p "Do you want to install the latest development version? (y/n)? " magic - case "$magic" in - y|Y) - printf "OK! You´re a risky person ;D\n" - NIGHTLY=1 +ARGS(){ + for i in "$@"; do + case "$i" in + --prefix=*) + VAR_PREFIX="${i#*=}" ;; - n|N) - printf "That´s OK - installing the latest stable version for you ;-)\n" - NIGHTLY=0 + --plugins=*) + VAR_PLUGINS="${i#*=}" ;; - *) - printf "Invalid input\n" - ERROR + --defcfg=*) + VAR_DEFAULT_CFG="${i#*=}" ;; - esac - - # Roll back to last tag if we're not on a nightly build - if [ "$NIGHTLY" != 1 ]; then - printf "Finding latest stable version...\n" - Iversion=$(git describe --abbrev=0) - printf "Starting Git checkout to %s...\n" "$Iversion" - git checkout -f -q $Iversion - fi + --examples=*) + VAR_EXAMPLE_CFGS="${i#*=}" + ;; + --dev) + NIGHTLY=1 + ;; + -d|--default) + assign_defaults + ;; + -fu|--forceupdate) + UPDATER_FORCE="1" + ;; + --install-updater|--selfupdate) + NIGHTLY=1 prepare_repo + install_script + exit 0 + ;; + --install-dependencies) + install_dependencies + exit 0 + ;; + -h|--help|*) + assign_defaults + printf "${bold}Usage:${normal} ${0} ${c_green}[OPTIONS]${normal}" + printf "\n\t${c_green}--prefix=${normal}${c_red}<path>${normal}\t\tSet the installation prefix.\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PREFIX" + printf "\n\t${c_green}--plugins=${normal}${c_red}<path>${normal}\tSet the plugin install path.\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PLUGINS" + printf "\n\t${c_green}--defcfg=${normal}${c_red}<path>${normal}\t\tSet the default configuration path.\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_DEFAULT_CFG" + printf "\n\t${c_green}--examples=${normal}${c_red}<path>${normal}\tSet the path for example configurations.\t${c_mag}Default:${normal} ${dim}%s${normal}\n" "$VAR_EXAMPLE_CFGS" + printf "\n\t${c_green}--dev${normal}\t\t\tInstall nightly version." + printf "\n\t${c_green}-d,\t--default${normal}\tUse default values to install." + printf "\n\t${c_green}-fu,\t--forceupdate${normal}\tForce the updater to update without a version check." + printf "\n\t${c_green}--selfupdate${normal}\t\tUpdates this script to the newest version and exit." + printf "\n\t${c_green}--install-updater${normal}\tInstall the updater (Run with midimonster-updater) and exit." + printf "\n\t${c_green}--install-dependencies${normal}\tInstall dependencies and exit" + printf "\n\t${c_green}-h,\t--help${normal}\t\tShow this message and exit." + printf "\n\t${uline}${bold}${c_mag}Each argument can be overwritten by another, the last one is used!.${normal}\n" + rmdir "$tmp_path" + exit 0 + ;; + esac + shift + done } -INSTALL-PREP () { - ( - printf "Starting download...\n" - git clone https://github.com/cbdevnet/midimonster.git "$tmp_path" # Gets Midimonster - printf "\nInitializing repository...\n" - cd $tmp_path - git init $tmp_path - printf "\n" - ) - NIGHTLY_CHECK - printf "Preparation successful\n\n" - printf "${bold}If you don't know what you're doing, just hit enter 4 times.${normal}\n" - - read -e -i "$VAR_PREFIX" -p "PREFIX (Install root directory): " input # Reads VAR_PREFIX - VAR_PREFIX="${input:-$VAR_PREFIX}" - - read -e -i "$VAR_PLUGINS" -p "PLUGINS (Plugin directory): " input # Reads VAR_PLUGINS - VAR_PLUGINS="${input:-$VAR_PLUGINS}" - - read -e -i "$VAR_DEFAULT_CFG" -p "Default config path: " input # Reads VAR_DEFAULT_CFG - VAR_DEFAULT_CFG="${input:-$VAR_DEFAULT_CFG}" - - read -e -i "$VAR_EXAMPLE_CFGS" -p "Example config directory: " input # Reads VAR_EXAMPLE_CFGS - VAR_EXAMPLE_CFGS="${input:-$VAR_EXAMPLE_CFGS}" - - UPDATER_SAVE - - export PREFIX=$VAR_PREFIX - export PLUGINS=$VAR_PLUGINS - export DEFAULT_CFG=$VAR_DEFAULT_CFG - export DESTDIR=$VAR_DESTDIR - export EXAMPLES=$VAR_EXAMPLE_CFGS +# Install unmatched dependencies +install_dependencies(){ + for dependency in ${deps[@]}; do + if [ "$(dpkg-query -W -f='${Status}' "$dependency" 2>/dev/null | grep -c "ok installed")" -eq 0 ]; then + printf "Installing %s\n" "$dependency" + apt-get install "$dependency" + else + printf "%s already installed!\n" "$dependency" + fi + done + printf "\n" } -UPDATER-PREP () { - ( - printf "Starting download...\n" - git clone https://github.com/cbdevnet/midimonster.git "$tmp_path" # Gets Midimonster - printf "\nInitializing repository...\n" - cd $tmp_path - git init $tmp_path - printf "Successfully imported settings from %s\n" "$updater_file" - ) - NIGHTLY_CHECK - printf "Preparation successful\n\n" - - rm -f "$VAR_PREFIX/bin/midimonster" - rm -rf "$VAR_PLUGINS/" - - UPDATER_SAVE - - export PREFIX=$VAR_PREFIX - export PLUGINS=$VAR_PLUGINS - export DEFAULT_CFG=$VAR_DEFAULT_CFG - export DESTDIR=$VAR_DESTDIR - export EXAMPLES=$VAR_EXAMPLE_CFGS +ask_questions(){ + # Only say if necessary + if [ -n "$VAR_PREFIX" ] || [ -n "$VAR_PLUGINS" ] || [ -n "$VAR_DEFAULT_CFG" ] || [ -n "$VAR_EXAMPLE_CFGS" ]; then + printf "${bold}If you don't know what you're doing, just hit enter a few times.${normal}\n\n" + fi + + if [ -z "$VAR_PREFIX" ]; then + read -e -i "$DEFAULT_PREFIX" -p "PREFIX (Install root directory): " input + VAR_PREFIX="${input:-$VAR_PREFIX}" + fi + + if [ -z "$VAR_PLUGINS" ]; then + read -e -i "$VAR_PREFIX$DEFAULT_PLUGINPATH" -p "PLUGINS (Plugin directory): " input + VAR_PLUGINS="${input:-$VAR_PLUGINS}" + fi + + if [ -z "$VAR_DEFAULT_CFG" ]; then + read -e -i "$DEFAULT_CFGPATH" -p "Default config path: " input + VAR_DEFAULT_CFG="${input:-$VAR_DEFAULT_CFG}" + fi + + if [ -z "$VAR_EXAMPLE_CFGS" ]; then + read -e -i "$VAR_PREFIX$DEFAULT_EXAMPLES" -p "Example config directory: " input + VAR_EXAMPLE_CFGS="${input:-$VAR_EXAMPLE_CFGS}" + fi } -UPDATER () { - installed_version="$(midimonster --version)" - #installed_version="MIDIMonster v0.3-40-gafed325" # FOR TESTING ONLY! (or bypassing updater version check) - if [[ "$installed_version" =~ "$latest_version" ]]; then - printf "Newest Version is already installed! ${bold}($installed_version)${normal}\n\n" - ERROR - else - printf "The installed Version ${bold}´$installed_version´${normal} equals not the newest stable version ${bold}´$latest_version´${normal} (Maybe you are running a development version?)\n\n" +# Clone the repository and select the correct version +prepare_repo(){ + printf "Cloning the repository\n" + git clone "https://github.com/cbdevnet/midimonster.git" "$tmp_path" + + # If not set via argument, ask whether to install development build + if [ -z "$NIGHTLY" ]; then + read -p "Do you want to install the latest development version? (y/n)? " magic + case "$magic" in + y|Y) + printf "OK! You´re a risky person ;D\n" + NIGHTLY=1 + ;; + n|N) + printf "That´s OK - installing the latest stable version for you ;-)\n" + NIGHTLY=0 + ;; + *) + printf "${bold}Invalid input -- INSTALLING LATEST STABLE VERSION!${normal}\n" + NIGHTLY=0 + ;; + esac fi - UPDATER-PREP - INSTALL-RUN - DONE + # Roll back to last tag if a stable version was requested + if [ "$NIGHTLY" != 1 ]; then + cd "$tmp_path" + printf "Finding latest stable version...\n" + last_tag=$(git describe --abbrev=0) + printf "Checking out %s...\n" "$last_tag" + git checkout -f -q "$last_tag" + fi } -INSTALL-RUN () { # Build +# Build and install the software +build(){ + # Export variables for make + export PREFIX="$VAR_PREFIX" + export PLUGINS="$VAR_PLUGINS" + export DEFAULT_CFG="$VAR_DEFAULT_CFG" + export EXAMPLES="$VAR_EXAMPLE_CFGS" + cd "$tmp_path" make clean - make $makeargs + make "$makeargs" make install } -UPDATER_SAVE () { # Saves file for the auto updater in this script - rm -rf $updater_dir - printf "Saving updater to %s/updater.sh\n" "$update_dir" +# Save data for the updater +save_config(){ + rm -f "$updater_dir/updater.conf" + mkdir -p "$updater_dir" + printf "Exporting updater config\n" + printf "VAR_PREFIX=%s\nVAR_PLUGINS=%s\nVAR_DEFAULT_CFG=%s\nVAR_DESTDIR=%s\nVAR_EXAMPLE_CFGS=%s\n" "$VAR_PREFIX" "$VAR_PLUGINS" "$VAR_DEFAULT_CFG" "$VAR_DESTDIR" "$VAR_EXAMPLE_CFGS" > "$updater_dir/updater.conf" +} + +# Updates this script using the one from the checked out repo (containing the requested version) +install_script(){ mkdir -p "$updater_dir" - wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh -O $updater_dir/updater.sh - printf "Creating symlink to updater in /usr/bin/midimonster-updater\n" - ln -s "$updater_dir/updater.sh" "/usr/bin/midimonster-updater" + printf "Copying updater to %s/updater.sh\n" "$updater_dir" + cp "$tmp_path/installer.sh" "$updater_dir/updater.sh" chmod +x "$updater_dir/updater.sh" - printf "Exporting updater config to %s\n" "$updater_file" - printf "VAR_PREFIX=%s\nVAR_PLUGINS=%s\nVAR_DEFAULT_CFG=%s\nVAR_DESTDIR=%s\nVAR_EXAMPLE_CFGS=%s\n" "$VAR_PREFIX" "$VAR_PLUGINS" "$VAR_DEFAULT_CFG" "$VAR_DESTDIR" "$VAR_EXAMPLE_CFGS" > $updater_file + printf "Creating symlink /usr/bin/midimonster-updater\n" + ln -s "$updater_dir/updater.sh" "/usr/bin/midimonster-updater" } -ERROR () { - printf "\nAborting...\n" - CLEAN - printf "Exiting...\n" +error_handler(){ + printf "\nAborting\n" exit 1 } -DONE () { - printf "\nDone.\n" - CLEAN - exit 0 -} - -CLEAN () { - printf "\nCleaning...\n" - rm -rf $tmp_path +cleanup(){ + if [ -d "$tmp_path" ]; then + printf "Cleaning up temporary files...\n" + rm -rf "$tmp_path" + fi } -############################################## FUNCTIONS ############################################## - - ################################################ Main ################################################# -trap ERROR SIGINT SIGTERM SIGKILL +trap error_handler SIGINT SIGTERM +trap cleanup EXIT + +# Parse arguments +ARGS "$@" clear -# Check if $user = root! +# Check whether we have the privileges to install stuff if [ "$user" != "root" ]; then - printf "Installer must be run as root\n" - ERROR + printf "The installer/updater requires root privileges to install the midimonster system-wide\n" + exit 1 fi -if [ $(wget -q --spider http://github.com) $? -eq 1 ]; then - printf "You need connection to the internet\n" - ERROR +# Check if we can download the sources +if [ "$(wget -q --spider http://github.com)" ]; then + printf "The installer/updater requires internet connectivity to download the midimonster sources\n" + exit 1 fi -# Check if updater config file exist and import it (overwrites default values!) -if [ -f $updater_file ]; then - printf "Starting updater...\n\n" - . $updater_file +# Check whether the updater needs to run +if [ -f "$updater_dir/updater.conf" ] || [ "$UPDATER_FORCE" = "1" ]; then + if [ -f "$updater_dir/updater.conf" ]; then + . "$updater_dir/updater.conf" + # Parse arguments again to compensate overwrite from source + ARGS "$@" + printf "Imported settings from %s/updater.conf\n" "$updater_dir" + fi - # Check if binary $updater/bin/midimonster exist. If yes start updater else skip. - if [ -x "$VAR_PREFIX/bin/midimonster" ]; then - UPDATER - else - printf "midimonster binary not found, skipping updater.\n" + if [ -n "$UPDATER_FORCE" ]; then + printf "Forcing the updater to start...\n\n" + elif [ -x "$VAR_PREFIX/bin/midimonster" ]; then + installed_version="$(midimonster --version)" + if [[ "$installed_version" =~ "$latest_version" ]]; then + printf "The installed version ${bold}$installed_version${normal} seems to be up to date\nDoing nothing\n\n" + exit 0 + else + printf "The installed version ${bold}$installed_version${normal} does not match the latest version ${bold}$latest_version${normal}\nMaybe you are running a development version?\n\n" + fi fi + + # Run updater steps + prepare_repo + install_script + save_config + build +else + # Run installer steps + install_dependencies + prepare_repo + ask_questions + install_script + save_config + build fi +exit 0 -INSTALL-DEPS -INSTALL-PREP -printf "\n" -INSTALL-RUN -DONE diff --git a/midimonster.c b/midimonster.c index b8594b4..b418711 100644 --- a/midimonster.c +++ b/midimonster.c @@ -9,6 +9,8 @@ #else #define MM_API __attribute__((dllexport)) #endif +#define BACKEND_NAME "core" +#define MM_SWAP_LIMIT 20 #include "midimonster.h" #include "config.h" #include "backend.h" @@ -28,25 +30,34 @@ typedef struct /*_mm_channel_mapping*/ { channel** to; } channel_mapping; -static size_t mappings = 0; -static channel_mapping* map = NULL; +static struct { + //routing_hash is set up for 256 buckets + size_t entries[256]; + channel_mapping* map[256]; + + event_collection pool[2]; + event_collection* events; +} routing = { + .events = routing.pool +}; + static size_t fds = 0; static managed_fd* fd = NULL; static volatile sig_atomic_t fd_set_dirty = 1; static uint64_t global_timestamp = 0; -static event_collection event_pool[2] = { - {0}, - {0} -}; -static event_collection* primary = event_pool; - volatile static sig_atomic_t shutdown_requested = 0; static void signal_handler(int signum){ shutdown_requested = 1; } +static size_t routing_hash(channel* key){ + uint64_t repr = (uint64_t) key; + //return 8bit hash for 256 buckets, not ideal but it works + return (repr ^ (repr >> 8) ^ (repr >> 16) ^ (repr >> 24) ^ (repr >> 32)) & 0xFF; +} + MM_API uint64_t mm_timestamp(){ return global_timestamp; } @@ -66,53 +77,66 @@ static void update_timestamp(){ } int mm_map_channel(channel* from, channel* to){ - size_t u, m; + size_t u, m, bucket = routing_hash(from); + //find existing source mapping - for(u = 0; u < mappings; u++){ - if(map[u].from == from){ + for(u = 0; u < routing.entries[bucket]; u++){ + if(routing.map[bucket][u].from == from){ break; } } //create new entry - if(u == mappings){ - map = realloc(map, (mappings + 1) * sizeof(channel_mapping)); - if(!map){ + if(u == routing.entries[bucket]){ + routing.map[bucket] = realloc(routing.map[bucket], (routing.entries[bucket] + 1) * sizeof(channel_mapping)); + if(!routing.map[bucket]){ + routing.entries[bucket] = 0; fprintf(stderr, "Failed to allocate memory\n"); return 1; } - memset(map + mappings, 0, sizeof(channel_mapping)); - mappings++; - map[u].from = from; + + memset(routing.map[bucket] + routing.entries[bucket], 0, sizeof(channel_mapping)); + routing.entries[bucket]++; + routing.map[bucket][u].from = from; } //check whether the target is already mapped - for(m = 0; m < map[u].destinations; m++){ - if(map[u].to[m] == to){ + for(m = 0; m < routing.map[bucket][u].destinations; m++){ + if(routing.map[bucket][u].to[m] == to){ return 0; } } - map[u].to = realloc(map[u].to, (map[u].destinations + 1) * sizeof(channel*)); - if(!map[u].to){ + //add a mapping target + routing.map[bucket][u].to = realloc(routing.map[bucket][u].to, (routing.map[bucket][u].destinations + 1) * sizeof(channel*)); + if(!routing.map[bucket][u].to){ fprintf(stderr, "Failed to allocate memory\n"); - map[u].destinations = 0; + routing.map[bucket][u].destinations = 0; return 1; } - map[u].to[map[u].destinations] = to; - map[u].destinations++; + routing.map[bucket][u].to[routing.map[bucket][u].destinations] = to; + routing.map[bucket][u].destinations++; return 0; } -static void map_free(){ - size_t u; - for(u = 0; u < mappings; u++){ - free(map[u].to); +static void routing_cleanup(){ + size_t u, n; + + for(u = 0; u < sizeof(routing.map) / sizeof(routing.map[0]); u++){ + for(n = 0; n < routing.entries[u]; n++){ + free(routing.map[u][n].to); + } + free(routing.map[u]); + routing.map[u] = NULL; + routing.entries[u] = 0; + } + + for(u = 0; u < sizeof(routing.pool) / sizeof(routing.pool[0]); u++){ + free(routing.pool[u].channel); + free(routing.pool[u].value); + routing.pool[u].alloc = 0; } - free(map); - mappings = 0; - map = NULL; } MM_API int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ @@ -169,7 +193,6 @@ MM_API int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ static void fds_free(){ size_t u; for(u = 0; u < fds; u++){ - //TODO free impl if(fd[u].fd >= 0){ close(fd[u].fd); fd[u].fd = -1; @@ -181,56 +204,46 @@ static void fds_free(){ } MM_API int mm_channel_event(channel* c, channel_value v){ - size_t u, p; + size_t u, p, bucket = routing_hash(c); //find mapped channels - for(u = 0; u < mappings; u++){ - if(map[u].from == c){ + for(u = 0; u < routing.entries[bucket]; u++){ + if(routing.map[bucket][u].from == c){ break; } } - if(u == mappings){ + if(u == routing.entries[bucket]){ //target-only channel return 0; } //resize event structures to fit additional events - if(primary->n + map[u].destinations >= primary->alloc){ - primary->channel = realloc(primary->channel, (primary->alloc + map[u].destinations) * sizeof(channel*)); - primary->value = realloc(primary->value, (primary->alloc + map[u].destinations) * sizeof(channel_value)); + if(routing.events->n + routing.map[bucket][u].destinations >= routing.events->alloc){ + routing.events->channel = realloc(routing.events->channel, (routing.events->alloc + routing.map[bucket][u].destinations) * sizeof(channel*)); + routing.events->value = realloc(routing.events->value, (routing.events->alloc + routing.map[bucket][u].destinations) * sizeof(channel_value)); - if(!primary->channel || !primary->value){ + if(!routing.events->channel || !routing.events->value){ fprintf(stderr, "Failed to allocate memory\n"); - primary->alloc = 0; - primary->n = 0; + routing.events->alloc = 0; + routing.events->n = 0; return 1; } - primary->alloc += map[u].destinations; + routing.events->alloc += routing.map[bucket][u].destinations; } //enqueue channel events //FIXME this might lead to one channel being mentioned multiple times in an apply call - for(p = 0; p < map[u].destinations; p++){ - primary->channel[primary->n + p] = map[u].to[p]; - primary->value[primary->n + p] = v; + memcpy(routing.events->channel + routing.events->n, routing.map[bucket][u].to, routing.map[bucket][u].destinations * sizeof(channel*)); + for(p = 0; p < routing.map[bucket][u].destinations; p++){ + routing.events->value[routing.events->n + p] = v; } - primary->n += map[u].destinations; + routing.events->n += routing.map[bucket][u].destinations; return 0; } -static void event_free(){ - size_t u; - - for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ - free(event_pool[u].channel); - free(event_pool[u].value); - event_pool[u].alloc = 0; - } -} - static void version(){ printf("MIDIMonster %s\n", MIDIMONSTER_VERSION); } @@ -271,6 +284,9 @@ static int platform_initialize(){ if(WSAStartup(version, &wsa)){ return 1; } + + unsigned error_mode = SetErrorMode(0); + SetErrorMode(error_mode | SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX); #endif return 0; } @@ -325,67 +341,68 @@ static int args_parse(int argc, char** argv, char** cfg_file){ return 0; } -int main(int argc, char** argv){ - fd_set all_fds, read_fds; +static int core_process(size_t nfds, managed_fd* signaled_fds){ event_collection* secondary = NULL; - struct timeval tv; - size_t u, n; - managed_fd* signaled_fds = NULL; - int rv = EXIT_FAILURE, error, maxfd = -1; - char* cfg_file = DEFAULT_CFG; - #ifdef _WIN32 - char* error_message = NULL; - #endif + size_t u, swaps = 0; - //parse commandline arguments - if(args_parse(argc, argv, &cfg_file)){ - return EXIT_FAILURE; + //run backend processing, collect events + DBGPF("%lu backend FDs signaled\n", nfds); + if(backends_handle(nfds, signaled_fds)){ + return 1; } - if(platform_initialize()){ - fprintf(stderr, "Failed to perform platform-specific initialization\n"); - return EXIT_FAILURE; - } + //limit number of collector swaps per iteration to prevent complete deadlock + while(routing.events->n && swaps < MM_SWAP_LIMIT){ + //swap primary and secondary event collectors + DBGPF("Swapping event collectors, %lu events in primary\n", routing.events->n); + for(u = 0; u < sizeof(routing.pool) / sizeof(routing.pool[0]); u++){ + if(routing.events != routing.pool + u){ + secondary = routing.events; + routing.events = routing.pool + u; + break; + } + } - FD_ZERO(&all_fds); - //initialize backends - if(plugins_load(PLUGINS)){ - fprintf(stderr, "Failed to initialize a backend\n"); - goto bail; + //push collected events to target backends + if(secondary->n && backends_notify(secondary->n, secondary->channel, secondary->value)){ + fprintf(stderr, "Backends failed to handle output\n"); + return 1; + } + + //reset the event count + secondary->n = 0; } - //read config - if(config_read(cfg_file)){ - fprintf(stderr, "Failed to read configuration file %s\n", cfg_file); - backends_stop(); - channels_free(); - instances_free(); - map_free(); - fds_free(); - plugins_close(); - config_free(); - return (usage(argv[0]) | platform_shutdown()); + if(swaps == MM_SWAP_LIMIT){ + LOG("Iteration swap limit hit, a backend may be configured to route events in an infinite loop"); } - //load an initial timestamp - update_timestamp(); + return 0; +} - //start backends - if(backends_start()){ - goto bail; - } +static int core_loop(){ + fd_set all_fds, read_fds; + managed_fd* signaled_fds = NULL; + struct timeval tv; + int error, maxfd = -1; + size_t n, u; + #ifdef _WIN32 + char* error_message = NULL; + #else + struct timespec ts; + #endif - signal(SIGINT, signal_handler); + FD_ZERO(&all_fds); //process events while(!shutdown_requested){ //rebuild fd set if necessary - if(fd_set_dirty){ + if(fd_set_dirty || !signaled_fds){ all_fds = fds_collect(&maxfd); signaled_fds = realloc(signaled_fds, fds * sizeof(managed_fd)); if(!signaled_fds){ fprintf(stderr, "Failed to allocate memory\n"); - goto bail; + return 1; } fd_set_dirty = 0; } @@ -393,20 +410,38 @@ int main(int argc, char** argv){ //wait for & translate events read_fds = all_fds; tv = backend_timeout(); - error = select(maxfd + 1, &read_fds, NULL, NULL, &tv); - if(error < 0){ - #ifndef _WIN32 - fprintf(stderr, "select failed: %s\n", strerror(errno)); + + //check whether there are any fds active, windows does not like select() without descriptors + if(maxfd >= 0){ + error = select(maxfd + 1, &read_fds, NULL, NULL, &tv); + if(error < 0){ + #ifndef _WIN32 + fprintf(stderr, "select failed: %s\n", strerror(errno)); + #else + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error_message, 0, NULL); + fprintf(stderr, "select failed: %s\n", error_message); + LocalFree(error_message); + error_message = NULL; + #endif + free(signaled_fds); + return 1; + } + } + else{ + DBGPF("No descriptors, sleeping for %zu msec", tv.tv_sec * 1000 + tv.tv_usec / 1000); + #ifdef _WIN32 + Sleep(tv.tv_sec * 1000 + tv.tv_usec / 1000); #else - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error_message, 0, NULL); - fprintf(stderr, "select failed: %s\n", error_message); - LocalFree(error_message); - error_message = NULL; + ts.tv_sec = tv.tv_sec; + ts.tv_nsec = tv.tv_usec * 1000; + nanosleep(&ts, NULL); #endif - break; } + //update this iteration's timestamp + update_timestamp(); + //find all signaled fds n = 0; for(u = 0; u < fds; u++){ @@ -416,47 +451,81 @@ int main(int argc, char** argv){ } } - //update this iteration's timestamp - update_timestamp(); - - //run backend processing, collect events - DBGPF("%lu backend FDs signaled\n", n); - if(backends_handle(n, signaled_fds)){ - goto bail; + //fetch and process events + if(core_process(n, signaled_fds)){ + free(signaled_fds); + return 1; } + } - while(primary->n){ - //swap primary and secondary event collectors - DBGPF("Swapping event collectors, %lu events in primary\n", primary->n); - for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ - if(primary != event_pool + u){ - secondary = primary; - primary = event_pool + u; - break; - } - } + free(signaled_fds); + return 0; +} - //push collected events to target backends - if(secondary->n && backends_notify(secondary->n, secondary->channel, secondary->value)){ - fprintf(stderr, "Backends failed to handle output\n"); - goto bail; - } +int main(int argc, char** argv){ + int rv = EXIT_FAILURE; + char* cfg_file = DEFAULT_CFG; + size_t u, n = 0, max = 0; - //reset the event count - secondary->n = 0; - } + //parse commandline arguments + if(args_parse(argc, argv, &cfg_file)){ + return EXIT_FAILURE; + } + + if(platform_initialize()){ + fprintf(stderr, "Failed to perform platform-specific initialization\n"); + return EXIT_FAILURE; + } + + //initialize backends + if(plugins_load(PLUGINS)){ + fprintf(stderr, "Failed to initialize a backend\n"); + goto bail; + } + + //read config + if(config_read(cfg_file)){ + fprintf(stderr, "Failed to parse master configuration file %s\n", cfg_file); + backends_stop(); + routing_cleanup(); + fds_free(); + plugins_close(); + config_free(); + return (usage(argv[0]) | platform_shutdown()); + } + + //load an initial timestamp + update_timestamp(); + + //start backends + if(backends_start()){ + goto bail; + } + + signal(SIGINT, signal_handler); + + //count and report mappings + for(u = 0; u < sizeof(routing.map) / sizeof(routing.map[0]); u++){ + n += routing.entries[u]; + max = max(max, routing.entries[u]); + } + LOGPF("Routing %" PRIsize_t " sources, largest bucket has %" PRIsize_t " entries", + n, max); + + if(!fds){ + fprintf(stderr, "No descriptors registered for multiplexing\n"); + } + + //run the core loop + if(!core_loop()){ + rv = EXIT_SUCCESS; } - rv = EXIT_SUCCESS; bail: //free all data - free(signaled_fds); backends_stop(); - channels_free(); - instances_free(); - map_free(); + routing_cleanup(); fds_free(); - event_free(); plugins_close(); config_free(); platform_shutdown(); diff --git a/midimonster.h b/midimonster.h index 2c29956..75eb30a 100644 --- a/midimonster.h +++ b/midimonster.h @@ -7,7 +7,7 @@ /* Core version unless set by the build process */ #ifndef MIDIMONSTER_VERSION - #define MIDIMONSTER_VERSION "v0.4-dist" + #define MIDIMONSTER_VERSION "v0.5-dist" #endif /* Set backend name if unset */ @@ -192,8 +192,8 @@ typedef struct _backend_instance { /* * Instance channel structure - * Backends may either manage their own channel registry - * or use the memory returned by mm_channel() + * Backends may either manage their own channel registry or use the global + * channel store via the mm_channel() API */ typedef struct _backend_channel { instance* instance; @@ -218,39 +218,41 @@ MM_API int mm_backend_register(backend b); /* * Finds an instance matching the specified backend and identifier. - * Since setting an identifier for an instance is optional, - * this may not work depending on the backend. - * Instance identifiers may for example be set in the backends - * mmbackend_start call. + * Since setting an identifier for an instance is optional, this may not work + * depending on the backend. Instance identifiers may for example be set in the + * backends mmbackend_start call. */ MM_API instance* mm_instance_find(char* backend, uint64_t ident); /* - * Provides a pointer to a channel structure, pre-filled with - * the provided instance reference and identifier. - * The `create` parameter is a boolean flag indicating whether - * a channel matching the `ident` parameter should be created if - * none exists. If the instance already registered a channel - * matching `ident`, a pointer to it is returned. - * This API is just a convenience function. The array of channels is - * only used for mapping internally, creating and managing your own - * channel store is possible. + * Provides a pointer to a channel structure, pre-filled with the provided + * instance reference and identifier. + * The `create` parameter is a boolean flag indicating whether a channel + * matching the `ident` parameter should be created in the global channel store + * if none exists yet. If the instance already registered a channel matching + * `ident`, a pointer to the existing channel is returned. + * This API is just a convenience function. Creating and managing a + * backend-internal channel store is possible (and encouraged for performance + * reasons). When returning pointers from a backend-local channel store, the + * returned pointers must stay valid over the lifetime of the instance and + * provide valid `instance` members, as they are used for callbacks. * For each channel with a non-NULL `impl` field registered using * this function, the backend will receive a call to its channel_free - * function. + * function (if it exists). */ MM_API channel* mm_channel(instance* i, uint64_t ident, uint8_t create); /* - * Register (manage = 1) or unregister (manage = 0) a file descriptor - * to be selected on. The backend will be notified when the descriptor - * becomes ready to read via its registered mmbackend_process_fd call. + * Register (manage = 1) or unregister (manage = 0) a file descriptor to be + * selected on. The backend will be notified when the descriptor becomes ready + * to read via its registered mmbackend_process_fd call. The `impl` argument + * will be provided within the corresponding managed_fd structure upon callback. */ MM_API int mm_manage_fd(int fd, char* backend, int manage, void* impl); /* - * Notifies the core of a channel event. Called by backends to - * inject events gathered from their backing implementation. + * Notifies the core of a channel event. Called by backends to inject events + * gathered from their backing implementation. */ MM_API int mm_channel_event(channel* c, channel_value v); @@ -262,14 +264,14 @@ MM_API int mm_backend_instances(char* backend, size_t* n, instance*** i); /* * Query an internal timestamp, which is updated every core iteration. - * This timestamp should not be used as a performance counter, but can be - * used for timeouting. Resolution is milliseconds. + * This timestamp should not be used as a performance counter, but can be used + * for timeouting. Resolution is milliseconds. */ MM_API uint64_t mm_timestamp(); /* - * Create a channel-to-channel mapping. This API should not - * be used by backends. It is only exported for core modules. + * Create a channel-to-channel mapping. This API should not be used by backends. + * It is only exported for core modules. */ int mm_map_channel(channel* from, channel* to); #endif @@ -51,7 +51,7 @@ static int plugin_attach(char* path, char* file){ char* error = NULL; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); - fprintf(stderr, "Failed to load plugin %s: %s\n", lib, error); + fprintf(stderr, "Failed to load plugin %s, check that all supporting libraries are present: %s\n", lib, error); LocalFree(error); #else fprintf(stderr, "Failed to load plugin %s: %s\n", lib, dlerror()); diff --git a/portability.h b/portability.h index f0bfd07..c249a10 100644 --- a/portability.h +++ b/portability.h @@ -18,6 +18,10 @@ #define htole64(x) OSSwapHostToLittleInt64(x) #define be64toh(x) OSSwapBigToHostInt64(x) #define le64toh(x) OSSwapLittleToHostInt64(x) + + #ifndef IPV6_ADD_MEMBERSHIP + #define IPV6_ADD_MEMBERSHIP IPV6_JOIN_GROUP + #endif #endif #ifdef _WIN32 |