From ff63873e236ef3c2b7d0a6b53aa60721ceca98c1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 27 Dec 2018 23:14:54 +0100 Subject: New configuration --- README.md | 1 + configs/osc-artnet.cfg | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 configs/osc-artnet.cfg diff --git a/README.md b/README.md index d2298b2..ef0069d 100644 --- a/README.md +++ b/README.md @@ -451,6 +451,7 @@ support for the protocols to translate. * libasound2-dev (for the MIDI backend) * libevdev-dev (for the evdev backend) +* pkg-config (as some projects and systems like to spread their files around) * A C compiler * GNUmake diff --git a/configs/osc-artnet.cfg b/configs/osc-artnet.cfg new file mode 100644 index 0000000..3eeba2e --- /dev/null +++ b/configs/osc-artnet.cfg @@ -0,0 +1,63 @@ +; This configuration maps the multifader page of the TouchOSC 'Mix 16' Layout +; to the first 48 ArtNet channels + +[backend artnet] +bind = 0.0.0.0 + +[osc touch] +bind = * 8000 +dest = learn@8001 + +[artnet out] +destination = 255.255.255.255 + +[map] +touch./4/multifader1/1 > out.1 +touch./4/multifader1/2 > out.2 +touch./4/multifader1/3 > out.3 +touch./4/multifader1/4 > out.4 +touch./4/multifader1/5 > out.5 +touch./4/multifader1/6 > out.6 +touch./4/multifader1/7 > out.7 +touch./4/multifader1/8 > out.8 +touch./4/multifader1/9 > out.9 +touch./4/multifader1/10 > out.10 +touch./4/multifader1/11 > out.11 +touch./4/multifader1/12 > out.12 +touch./4/multifader1/13 > out.13 +touch./4/multifader1/14 > out.14 +touch./4/multifader1/15 > out.15 +touch./4/multifader1/16 > out.16 +touch./4/multifader1/17 > out.17 +touch./4/multifader1/18 > out.18 +touch./4/multifader1/19 > out.19 +touch./4/multifader1/20 > out.20 +touch./4/multifader1/21 > out.21 +touch./4/multifader1/22 > out.22 +touch./4/multifader1/23 > out.23 +touch./4/multifader1/24 > out.24 + +touch./4/multifader2/1 > out.25 +touch./4/multifader2/2 > out.26 +touch./4/multifader2/3 > out.27 +touch./4/multifader2/4 > out.28 +touch./4/multifader2/5 > out.29 +touch./4/multifader2/6 > out.30 +touch./4/multifader2/7 > out.31 +touch./4/multifader2/8 > out.32 +touch./4/multifader2/9 > out.33 +touch./4/multifader2/10 > out.34 +touch./4/multifader2/11 > out.35 +touch./4/multifader2/12 > out.36 +touch./4/multifader2/13 > out.37 +touch./4/multifader2/14 > out.38 +touch./4/multifader2/15 > out.39 +touch./4/multifader2/16 > out.40 +touch./4/multifader2/17 > out.41 +touch./4/multifader2/18 > out.42 +touch./4/multifader2/19 > out.43 +touch./4/multifader2/20 > out.44 +touch./4/multifader2/21 > out.45 +touch./4/multifader2/22 > out.46 +touch./4/multifader2/23 > out.47 +touch./4/multifader2/24 > out.48 -- cgit v1.2.3 From b72d0964dd33b9084a0fc02d7fb365850c5606c6 Mon Sep 17 00:00:00 2001 From: Peter Newman Date: Wed, 6 Mar 2019 13:36:22 +0000 Subject: Add Travis CI (#15) * Add and enable Travis CI builds * Add build status to readme --- .travis-ci.sh | 79 +++++++++++++++++++++++++++ .travis.yml | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- 3 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 .travis-ci.sh create mode 100644 .travis.yml diff --git a/.travis-ci.sh b/.travis-ci.sh new file mode 100644 index 0000000..b3e0744 --- /dev/null +++ b/.travis-ci.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# This script is triggered from the script section of .travis.yml +# It runs the appropriate commands depending on the task requested. + +set -e + +COVERITY_SCAN_BUILD_URL="https://scan.coverity.com/scripts/travisci_build_coverity_scan.sh" + +SPELLINGBLACKLIST=$(cat <<-BLACKLIST + -wholename "./.git/*" +BLACKLIST +) + +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; + 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; + else + echo "Found $spellingerrors spelling errors via codespell" + fi; +elif [[ $TASK = 'coverity' ]]; then + # Run Coverity Scan unless token is zero length + # The Coverity Scan script also relies on a number of other COVERITY_SCAN_ + # variables set in .travis.yml + if [[ ${#COVERITY_SCAN_TOKEN} -ne 0 ]]; then + curl -s $COVERITY_SCAN_BUILD_URL | bash + else + echo "Skipping Coverity Scan as no token found, probably a Pull Request" + fi; +elif [[ $TASK = 'sanitize' ]]; then + # Run sanitized compile + travis_fold start "make_sanitize" + make sanitize; + travis_fold end "make_sanitize" +else + # Otherwise compile as normal + travis_fold start "make" + make; + travis_fold end "make" +fi diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1b7c2e2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,170 @@ +language: c +# Use the latest Travis images since they are more up to date than the stable release. +group: edge + +before_script: + - export -f travis_fold + +script: + - "bash -ex .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 + - *base_build + - libasound2-dev + - libevdev-dev + packages: &core_build_gpp_latest + - *core_build + - gcc-8 + packages: &core_build_clang_latest + - *core_build + - clang-6.0 + +matrix: + fast_finish: true + include: + - os: osx + osx_image: xcode10.2 + compiler: clang + env: + - TASK='compile' + - os: osx + osx_image: xcode10.2 + compiler: gcc + env: + - TASK='compile' + - os: osx + osx_image: xcode10.2 + compiler: clang + env: + - TASK='sanitize' + - os: linux + dist: xenial + compiler: clang + env: TASK='compile' + addons: + apt: + packages: + - *core_build_clang_latest + sources: + - ubuntu-toolchain-r-test + - llvm-toolchain-xenial-6.0 + - os: linux + dist: xenial + compiler: gcc + env: TASK='compile' + addons: + apt: + packages: + - *core_build_gpp_latest + sources: + - ubuntu-toolchain-r-test + - os: linux + dist: xenial + compiler: clang + env: TASK='sanitize' + addons: + apt: + packages: + - *core_build_clang_latest + sources: + - ubuntu-toolchain-r-test + - llvm-toolchain-xenial-6.0 + - os: linux + dist: xenial + compiler: gcc + env: TASK='coverity' + addons: + apt: + packages: + # Coverity doesn't work with g++-5 or g++-6 yet + - *core_build + - gcc-4.9 + sources: + - ubuntu-toolchain-r-test + - os: linux + dist: xenial + env: TASK='spellintian' + addons: + apt: + packages: + - *core_build + - moreutils + - os: linux + dist: xenial + env: TASK='spellintian-duplicates' + addons: + apt: + packages: + - *core_build + - moreutils + - os: linux + dist: xenial + env: TASK='codespell' + addons: + apt: + packages: + - *core_build + - moreutils + allow_failures: + - os: linux + dist: xenial + compiler: gcc + env: TASK='coverity' + - os: linux + dist: xenial + env: TASK='spellintian-duplicates' + +env: + global: + # No colours in terminal (to reduce log file size) + - TERM=dumb + # Parallel make build + - MAKEFLAGS="-j 2" + # -- BEGIN Coverity Scan ENV + - COVERITY_SCAN_BUILD_COMMAND_PREPEND="cov-configure --comptype gcc --compiler gcc-4.9 --template" + # The build command with all of the arguments that you would apply to a manual `cov-build` + # Usually this is the same as STANDARD_BUILD_COMMAND, excluding the automated test arguments + - COVERITY_SCAN_BUILD_COMMAND="make" + # Name of the project + - COVERITY_SCAN_PROJECT_NAME="$TRAVIS_REPO_SLUG" + # Email address for notifications related to this build + # - COVERITY_SCAN_NOTIFICATION_EMAIL="" + # Regular expression selects on which branches to run analysis + # Be aware of quotas. Do not run on every branch/commit + - COVERITY_SCAN_BRANCH_PATTERN=".*" + # COVERITY_SCAN_TOKEN via "travis encrypt" using the repo's public key + # - secure: "" + # -- END Coverity Scan ENV + +cache: + apt: true + directories: + - $HOME/.ccache # ccache 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: + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then PATH=/usr/local/opt/ccache/libexec:$PATH; fi # Use ccache on Mac too +#Coverity doesn't work with g++ 5 or 6, so only upgrade to g++ 4.9 for that + - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) -a "$CC" = "gcc" ]; then export CC="ccache gcc-8"; fi +#Use the latest clang if we're compiling with clang + - if [ "$TRAVIS_OS_NAME" == "linux" -a "$CC" = "clang" ]; then export CC="clang-6.0"; fi +#Report the compiler version + - $CC --version + - if [ "$TASK" == "spellintian" -o "$TASK" == "spellintian-duplicates" ]; then wget "http://archive.ubuntu.com/ubuntu/pool/main/l/lintian/lintian_2.5.104_all.deb"; sudo dpkg -i lintian_*.deb; sudo apt-get install -f -y; fi # Install a later lintian + +after_script: + - if [ "$TASK" = "coverity" ]; then tail -n 10000 ${TRAVIS_BUILD_DIR}/cov-int/build-log.txt; cat ${TRAVIS_BUILD_DIR}/cov-int/scm_log.txt; fi diff --git a/README.md b/README.md index ef0069d..095121c 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ on any other (or the same) supported protocol, for example to: * Control lighting fixtures or DAWs using gamepad controllers ([Example configuration](configs/evdev.conf)) * Play games or type using MIDI controllers -[![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) +[![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster) [![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) # Table of Contents -- cgit v1.2.3 From 90a99a3c095aa634e8768914e34d2cb84586615f Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 7 Mar 2019 00:13:04 +0100 Subject: Use _CLOCK_MONOTONIC_RAW for timestamps on OSX --- portability.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/portability.h b/portability.h index 71d72fc..25aee01 100644 --- a/portability.h +++ b/portability.h @@ -1,5 +1,7 @@ #ifdef __APPLE__ - #define CLOCK_MONOTONIC_RAW _CLOCK_MONOTONIC_RAW + #ifndef CLOCK_MONOTONIC_COARSE + #define CLOCK_MONOTONIC_COARSE _CLOCK_MONOTONIC_RAW + #endif #include #define htobe16(x) OSSwapHostToBigInt16(x) -- cgit v1.2.3 From 34a194cc101d7c77202b79a06e285573541d823f Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 16 Mar 2019 15:32:52 +0100 Subject: Only start backends with active instances --- backend.c | 10 +++++++++- midimonster.h | 9 ++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/backend.c b/backend.c index e9b12df..ed5e6db 100644 --- a/backend.c +++ b/backend.c @@ -251,8 +251,16 @@ int mm_backend_register(backend b){ int backends_start(){ int rv = 0, current; - size_t u; + size_t u, p; for(u = 0; u < nbackends; u++){ + //only start backends that have instances + for(p = 0; p < ninstances && instances[p]->backend != backends + u; p++){ + } + if(p == ninstances){ + fprintf(stderr, "Skipping start of backend %s\n", backends[u].name); + continue; + } + current = backends[u].start(); if(current){ fprintf(stderr, "Failed to start backend %s\n", backends[u].name); diff --git a/midimonster.h b/midimonster.h index 98d8459..a3e5e1f 100644 --- a/midimonster.h +++ b/midimonster.h @@ -29,7 +29,7 @@ struct _managed_fd; * * int init() * The only function that should be exported by the shared object. * Called when the shared object is attached. Should register - * a backend structure with the core. + * a backend structure containing callable entry points with the core. * Returning anything other than zero causes midimonster to fail the * startup checks. * * mmbackend_configure @@ -46,7 +46,8 @@ struct _managed_fd; * out-of-memory condition and terminates the program. * * mmbackend_start * Called after all instances have been created and all mappings - * have been set up. May be used to connect to backing hardware + * have been set up. Only backends for which instances have been configured + * receive the start call. May be used to connect to backing hardware * or to update runtime-specific data in the various data structures. * Returning a non-zero value signals an error starting the backend * and stops further progress. @@ -68,7 +69,9 @@ struct _managed_fd; * Return the maximum sleep interval for this backend in milliseconds. * If not implemented, a maximum interval of one second is used. * * mmbackend_shutdown - * Clean up all allocations, finalize all hardware connections. + * Clean up all allocations, finalize all hardware connections. All registered + * backends receive the shutdown call, regardless of whether they have been + * started previously. * Return value is currently ignored. */ typedef int (*mmbackend_handle_event)(struct _backend_instance* inst, size_t channels, struct _backend_channel** c, struct _channel_value* v); -- cgit v1.2.3 From f227a44b5442fc85ee66fd2f5b964b8405e6c49e Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 16 Mar 2019 15:40:52 +0100 Subject: Clarify wording of wide channels in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 095121c..c0b8184 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Example mapping: net1.231 < net2.123 ``` -A 16-bit channel (spanning any two normal channels in the same universe) may be mapped with the syntax +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax ``` net1.1+2 > net2.5+123 ``` @@ -187,7 +187,7 @@ Example mapping: sacn1.231 < sacn2.123 ``` -A 16-bit channel (spanning any two normal channels in the same universe) may be mapped with the syntax +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax ``` sacn.1+2 > sacn2.5+123 ``` -- cgit v1.2.3 From 28000d6e09d5372ada278bc8e8d9960c5f15e678 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 19 Mar 2019 00:31:47 +0100 Subject: Allow the fd set to be updated at translation time (Fixes #8) --- midimonster.c | 40 ++++++++++++++++++++++++++++++---------- midimonster.h | 2 -- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/midimonster.c b/midimonster.c index 629a0d9..798f06d 100644 --- a/midimonster.c +++ b/midimonster.c @@ -20,6 +20,7 @@ static size_t mappings = 0; static channel_mapping* map = NULL; 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] = { @@ -114,6 +115,7 @@ int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ fd[u].fd = -1; fd[u].backend = NULL; fd[u].impl = NULL; + fd_set_dirty = 1; } return 0; } @@ -143,6 +145,7 @@ int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ fd[u].fd = new_fd; fd[u].backend = b; fd[u].impl = impl; + fd_set_dirty = 1; return 0; } @@ -218,6 +221,28 @@ int usage(char* fn){ return EXIT_FAILURE; } +static fd_set fds_collect(int* max_fd){ + size_t u = 0; + fd_set rv_fds; + + if(max_fd){ + *max_fd = -1; + } + + DBGPF("Building selector set from %zu FDs registered to core\n", fds); + FD_ZERO(&rv_fds); + for(u = 0; u < fds; u++){ + if(fd[u].fd >= 0){ + FD_SET(fd[u].fd, &rv_fds); + if(max_fd){ + *max_fd = max(*max_fd, fd[u].fd); + } + } + } + + return rv_fds; +} + int main(int argc, char** argv){ fd_set all_fds, read_fds; event_collection* secondary = NULL; @@ -262,18 +287,13 @@ int main(int argc, char** argv){ goto bail; } - //create initial fd set - DBGPF("Building selector set from %zu FDs registered to core\n", fds); - FD_ZERO(&all_fds); - for(u = 0; u < fds; u++){ - if(fd[u].fd >= 0){ - FD_SET(fd[u].fd, &all_fds); - maxfd = max(maxfd, fd[u].fd); - } - } - //process events while(!shutdown_requested){ + //build fd set if necessary + if(fd_set_dirty){ + all_fds = fds_collect(&maxfd); + fd_set_dirty = 0; + } //wait for & translate events read_fds = all_fds; tv = backend_timeout(); diff --git a/midimonster.h b/midimonster.h index a3e5e1f..cffbf67 100644 --- a/midimonster.h +++ b/midimonster.h @@ -179,8 +179,6 @@ channel* mm_channel(instance* i, uint64_t ident, uint8_t create); /* * Register a file descriptor to be selected on. The backend * will be notified via the mmbackend_process_fd call. - * This function may only be called from within the mmbackend_start - * procedure. */ int mm_manage_fd(int fd, char* backend, int manage, void* impl); /* -- cgit v1.2.3 From 0928bc2c59a38c411a35c49d83fd468e5e0dc43c Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 19 Mar 2019 00:36:23 +0100 Subject: Documentation update --- midimonster.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/midimonster.h b/midimonster.h index cffbf67..572b5fb 100644 --- a/midimonster.h +++ b/midimonster.h @@ -177,12 +177,13 @@ instance* mm_instance_find(char* backend, uint64_t ident); channel* mm_channel(instance* i, uint64_t ident, uint8_t create); //TODO channel* mm_channel_find() /* - * Register a file descriptor to be selected on. The backend - * will be notified via the 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. */ int mm_manage_fd(int fd, char* backend, int manage, void* impl); /* - * Notifies the core of a channel event. Used by backends to + * Notifies the core of a channel event. Called by backends to * inject events gathered from their backing implementation. */ int mm_channel_event(channel* c, channel_value v); -- cgit v1.2.3 From 63643cc99a8d621a82111fd6d4d69f80379d2933 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 20 Mar 2019 20:23:12 +0100 Subject: Fix signaled fd storage reallocation --- midimonster.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/midimonster.c b/midimonster.c index 798f06d..00d11f3 100644 --- a/midimonster.c +++ b/midimonster.c @@ -280,20 +280,19 @@ int main(int argc, char** argv){ signal(SIGINT, signal_handler); - //allocate data buffers - signaled_fds = calloc(fds, sizeof(managed_fd)); - if(!signaled_fds){ - fprintf(stderr, "Failed to allocate memory\n"); - goto bail; - } - //process events while(!shutdown_requested){ - //build fd set if necessary + //rebuild fd set if necessary if(fd_set_dirty){ 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; + } fd_set_dirty = 0; } + //wait for & translate events read_fds = all_fds; tv = backend_timeout(); -- cgit v1.2.3 From 95f804bb5f8239d018e8fa440a2ca3e0111d4696 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 22 Mar 2019 21:16:41 +0100 Subject: Implement an OLA backend (Fixes #14) --- README.md | 60 ++++++++++- backends/Makefile | 13 ++- backends/ola.cpp | 318 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/ola.h | 38 +++++++ monster.cfg | 37 ++++--- 5 files changed, 443 insertions(+), 23 deletions(-) create mode 100644 backends/ola.cpp create mode 100644 backends/ola.h diff --git a/README.md b/README.md index c0b8184..3f5606a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Currently, the MIDIMonster supports the following protocols: * sACN / E1.31 * OSC * evdev input devices (Linux) +* Open Lighting Architecture (OLA) The MIDIMonster allows the user to translate any channel on one protocol into channel(s) on any other (or the same) supported protocol, for example to: @@ -48,18 +49,23 @@ on any other (or the same) supported protocol, for example to: - [Global configuration](#global-configuration-3) - [Instance configuration](#instance-configuration-3) - [Channel specification](#channel-specification-3) - - [Known bugs/problems](#known-bugs-problems) + - [Known bugs/problems](#known-bugs--problems-3) + [The `loopback` backend](#the-loopback-backend) - [Global configuration](#global-configuration-4) - [Instance configuration](#instance-configuration-4) - [Channel specification](#channel-specification-4) - - [Known bugs / problems](#known-bugs--problems-3) + - [Known bugs / problems](#known-bugs--problems-4) + [The `osc` backend](#the-osc-backend) - [Global configuration](#global-configuration-5) - [Instance configuration](#instance-configuration-5) - [Channel specification](#channel-specification-5) - [Supported types & value ranges](#supported-types--value-ranges) - - [Known bugs / problems](#known-bugs--problems-4) + - [Known bugs / problems](#known-bugs--problems-5) + + [The `ola` backend](#the-ola-backend) + - [Global configuration](#global-configuration-6) + - [Instance configuration](#instance-configuration-6) + - [Channel specification](#channel-specification-6) + - [Known bugs / problems](#known-bugs--problems-6) * [Building](#building) + [Prerequisites](#prerequisites) + [Build](#build) @@ -310,7 +316,7 @@ Note that to map an absolute axis on an output-enabled instance, additional info and maximum are required. These must be specified in the instance configuration. When only mapping the instance as a channel input, this is not required. -#### Known bugs/problems +#### Known bugs / problems Creating an `evdev` output device requires elevated privileges, namely, write access to the system's `/dev/uinput`. Usually, this is granted for users in the `input` group and the `root` user. @@ -439,6 +445,48 @@ The default ranges are: Ping requests are not yet answered. There may be some problems using broadcast output and input. +### The `ola` backend + +This backend connects the MIDIMonster to the Open Lighting Architecture daemon. This can be useful +to take advantage of additional protocols implemented in OLA. This backend is currently marked as +optional and is only built with `make full` in the `backends/` directory, as the OLA is a large +dependency to require for all users. + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|---------------|-------------------------------------------------------| +| `universe` | `7` | `0` | OLA universe to send/receive data on | + +#### Channel specification + +A channel is specified by it's universe index. Channel indices start at 1 and end at 512. + +Example mapping: +``` +ola1.231 < in2.123 +``` + +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax +``` +ola1.1+2 > net2.5+123 +``` + +A normal channel that is part of a wide channel can not be mapped individually. + +#### Known bugs / problems + +The backend currently assumes that the OLA daemon is running on the same host as the MIDIMonster. +This may be made configurable in the future. + +This backend requires `libola-dev` to be installed, which pulls in a rather large and aggressive (in terms of probing +and taking over connected hardware) daemon. It is thus marked as optional and only built when executing the `full` target +within the `backends` directory. + ## Building This section will explain how to build the provided sources to be able to run @@ -451,6 +499,7 @@ support for the protocols to translate. * libasound2-dev (for the MIDI backend) * libevdev-dev (for the evdev backend) +* libola-dev (for the optional OLA backend) * pkg-config (as some projects and systems like to spread their files around) * A C compiler * GNUmake @@ -459,6 +508,9 @@ support for the protocols to translate. Just running `make` in the source directory should do the trick. +Some backends have been marked as optional as they require rather large additional software to be installed, +for example the `ola` backend. To build these, run `make full` in the backends directory. + ## Development The architecture is split into the `midimonster` core, handling mapping diff --git a/backends/Makefile b/backends/Makefile index 446ad70..aef39c4 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,10 +1,12 @@ -.PHONY: all clean +.PHONY: all clean full +OPTIONAL_BACKENDS = ola.so LINUX_BACKENDS = midi.so evdev.so BACKENDS = artnet.so osc.so loopback.so sacn.so SYSTEM := $(shell uname -s) CFLAGS += -fPIC -I../ +CPPFLAGS += -fPIC -I../ LDFLAGS += -shared # Build Linux backends if possible @@ -19,11 +21,18 @@ endif midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) +ola.so: LDLIBS = -lola +ola.so: CPPFLAGS += -Wno-write-strings %.so :: %.c %.h $(CC) $(CFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS) +%.so :: %.cpp %.h + $(CXX) $(CPPFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS) + all: $(BACKENDS) +full: $(BACKENDS) $(OPTIONAL_BACKENDS) + clean: - $(RM) $(BACKENDS) + $(RM) $(BACKENDS) $(OPTIONAL_BACKENDS) diff --git a/backends/ola.cpp b/backends/ola.cpp new file mode 100644 index 0000000..299c883 --- /dev/null +++ b/backends/ola.cpp @@ -0,0 +1,318 @@ +#include "ola.h" +#include +#include +#include +#include +#include +#include +#include + +#define BACKEND_NAME "ola" +static ola::io::SelectServer* ola_select = NULL; +static ola::OlaCallbackClient* ola_client = NULL; + +int init(){ + backend ola = { + .name = BACKEND_NAME, + .conf = ola_configure, + .create = ola_instance, + .conf_instance = ola_configure_instance, + .channel = ola_channel, + .handle = ola_set, + .process = ola_handle, + .start = ola_start, + .shutdown = ola_shutdown + }; + + //register backend + if(mm_backend_register(ola)){ + fprintf(stderr, "Failed to register OLA backend\n"); + return 1; + } + + ola::InitLogging(ola::OLA_LOG_WARN, ola::OLA_LOG_STDERR); + return 0; +} + +static int ola_configure(char* option, char* value){ + fprintf(stderr, "Unknown OLA backend option %s\n", option); + return 1; +} + +static instance* ola_instance(){ + ola_instance_data* data = NULL; + instance* inst = mm_instance(); + if(!inst){ + return NULL; + } + + data = (ola_instance_data*)calloc(1, sizeof(ola_instance_data)); + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + inst->impl = data; + return inst; +} + +static int ola_configure_instance(instance* inst, char* option, char* value){ + ola_instance_data* data = (ola_instance_data*) inst->impl; + + if(!strcmp(option, "universe")){ + data->universe_id = strtoul(value, NULL, 0); + return 0; + } + + fprintf(stderr, "Unknown OLA option %s for instance %s\n", option, inst->name); + return 1; +} + +static channel* ola_channel(instance* inst, char* spec){ + ola_instance_data* data = (ola_instance_data*) inst->impl; + char* spec_next = spec; + unsigned chan_a = strtoul(spec, &spec_next, 10); + unsigned chan_b = 0; + + //primary channel sanity check + if(!chan_a || chan_a > 512){ + fprintf(stderr, "Invalid OLA channel specification %s\n", spec); + return NULL; + } + chan_a--; + + //secondary channel setup + if(*spec_next == '+'){ + chan_b = strtoul(spec_next + 1, NULL, 10); + if(!chan_b || chan_b > 512){ + fprintf(stderr, "Invalid wide-channel spec %s\n", spec); + return NULL; + } + chan_b--; + + //if mapped mode differs, bail + if(IS_ACTIVE(data->data.map[chan_b]) && data->data.map[chan_b] != (MAP_FINE | chan_a)){ + fprintf(stderr, "Fine channel already mapped for OLA spec %s\n", spec); + return NULL; + } + + data->data.map[chan_b] = MAP_FINE | chan_a; + } + + //check current map mode + if(IS_ACTIVE(data->data.map[chan_a])){ + if((*spec_next == '+' && data->data.map[chan_a] != (MAP_COARSE | chan_b)) + || (*spec_next != '+' && data->data.map[chan_a] != (MAP_SINGLE | chan_a))){ + fprintf(stderr, "Primary OLA channel already mapped at differing mode: %s\n", spec); + return NULL; + } + } + data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a); + + return mm_channel(inst, chan_a, 1); +} + +static int ola_set(instance* inst, size_t num, channel** c, channel_value* v){ + size_t u, mark = 0; + ola_instance_data* data = (ola_instance_data*) inst->impl; + + for(u = 0; u < num; u++){ + if(IS_WIDE(data->data.map[c[u]->ident])){ + uint32_t val = v[u].normalised * ((double) 0xFFFF); + //the primary (coarse) channel is the one registered to the core, so we don't have to check for that + if(data->data.data[c[u]->ident] != ((val >> 8) & 0xFF)){ + mark = 1; + data->data.data[c[u]->ident] = (val >> 8) & 0xFF; + } + + if(data->data.data[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){ + mark = 1; + data->data.data[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF; + } + } + else if(data->data.data[c[u]->ident] != (v[u].normalised * 255.0)){ + mark = 1; + data->data.data[c[u]->ident] = v[u].normalised * 255.0; + } + } + + if(mark){ + ola_client->SendDmx(data->universe_id, ola::DmxBuffer(data->data.data, 512)); + } + + return 0; +} + +static int ola_handle(size_t num, managed_fd* fds){ + if(!num){ + return 0; + } + + //defer input to ola via the scenic route... + ola_select->RunOnce(); + return 0; +} + +void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dmx, const std::string& error) { + size_t p, max_mark = 0; + //this should really be size_t but ola is weird... + unsigned int dmx_length = 512; + uint8_t raw_dmx[dmx_length]; + uint16_t wide_val; + channel* chan = NULL; + channel_value val; + instance* inst = mm_instance_find(BACKEND_NAME, universe); + if(!inst){ + return; + } + ola_instance_data* data = (ola_instance_data*) inst->impl; + ola_dmx.Get((uint8_t*)raw_dmx, &dmx_length); + + //read data into instance universe, mark changed channels + for(p = 0; p < dmx_length; p++){ + if(IS_ACTIVE(data->data.map[p]) && raw_dmx[p] != data->data.data[p]){ + data->data.data[p] = raw_dmx[p]; + data->data.map[p] |= MAP_MARK; + max_mark = p; + } + } + + //generate channel events + for(p = 0; p <= max_mark; p++){ + if(data->data.map[p] & MAP_MARK){ + data->data.map[p] &= ~MAP_MARK; + if(data->data.map[p] & MAP_FINE){ + chan = mm_channel(inst, MAPPED_CHANNEL(data->data.map[p]), 0); + } + else{ + chan = mm_channel(inst, p, 0); + } + + if(!chan){ + fprintf(stderr, "Active channel %zu on %s not known to core\n", p, inst->name); + return; + } + + if(IS_WIDE(data->data.map[p])){ + data->data.map[MAPPED_CHANNEL(data->data.map[p])] &= ~MAP_MARK; + wide_val = data->data.data[p] << ((data->data.map[p] & MAP_COARSE) ? 8 : 0); + wide_val |= data->data.data[MAPPED_CHANNEL(data->data.map[p])] << ((data->data.map[p] & MAP_COARSE) ? 0 : 8); + + val.raw.u64 = wide_val; + val.normalised = (double) wide_val / (double) 0xFFFF; + } + else{ + val.raw.u64 = data->data.data[p]; + val.normalised = (double) data->data.data[p] / 255.0; + } + + if(mm_channel_event(chan, val)){ + fprintf(stderr, "Failed to push OLA channel event to core\n"); + return; + } + } + } +} + +void ola_register_callback(const std::string &error) { + if(!error.empty()){ + fprintf(stderr, "OLA backend failed to register for universe: %s\n", error.c_str()); + } +} + +static int ola_start(){ + size_t n, u, p; + instance** inst = NULL; + ola_instance_data* data = NULL; + + ola_select = new ola::io::SelectServer(); + ola::network::IPV4SocketAddress ola_server(ola::network::IPV4Address::Loopback(), ola::OLA_DEFAULT_PORT); + ola::network::TCPSocket* ola_socket = ola::network::TCPSocket::Connect(ola_server); + if(!ola_socket){ + fprintf(stderr, "Failed to connect to OLA server\n"); + return 1; + } + + ola_client = new ola::OlaCallbackClient(ola_socket); + + if(!ola_client->Setup()){ + fprintf(stderr, "Failed to start OLA client\n"); + goto bail; + } + + ola_select->AddReadDescriptor(ola_socket); + + fprintf(stderr, "OLA backend registering %zu descriptors to core\n", 1); + if(mm_manage_fd(ola_socket->ReadDescriptor(), BACKEND_NAME, 1, NULL)){ + goto bail; + } + + ola_client->SetDmxCallback(ola::NewCallback(&ola_data_receive)); + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + goto bail; + } + + //this should not happen anymore (backends without instances are not started anymore) + if(!n){ + free(inst); + return 0; + } + + for(u = 0; u < n; u++){ + data = (ola_instance_data*) inst[u]->impl; + inst[u]->ident = data->universe_id; + + //check for duplicate instances (using the same universe) + for(p = 0; p < u; p++){ + if(inst[u]->ident == inst[p]->ident){ + fprintf(stderr, "OLA universe used in multiple instances, use one instance: %s - %s\n", inst[u]->name, inst[p]->name); + goto bail; + } + } + ola_client->RegisterUniverse(data->universe_id, ola::REGISTER, ola::NewSingleCallback(&ola_register_callback)); + } + + //run the ola select implementation to run all commands + ola_select->RunOnce(); + free(inst); + return 0; +bail: + free(inst); + delete ola_client; + ola_client = NULL; + delete ola_select; + ola_select = NULL; + return 1; +} + +static int ola_shutdown(){ + size_t n, p; + instance** inst = NULL; + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(p = 0; p < n; p++){ + free(inst[p]->impl); + } + free(inst); + + if(ola_client){ + ola_client->Stop(); + delete ola_client; + ola_client = NULL; + } + + if(ola_select){ + ola_select->Terminate(); + delete ola_select; + ola_select = NULL; + } + + fprintf(stderr, "OLA backend shut down\n"); + return 0; +} diff --git a/backends/ola.h b/backends/ola.h new file mode 100644 index 0000000..c943d52 --- /dev/null +++ b/backends/ola.h @@ -0,0 +1,38 @@ +extern "C" { + #include "midimonster.h" + //C++ has it's own implementation of these... + #undef min + #undef max + + int init(); + static int ola_configure(char* option, char* value); + static int ola_configure_instance(instance* instance, char* option, char* value); + static instance* ola_instance(); + static channel* ola_channel(instance* instance, char* spec); + static int ola_set(instance* inst, size_t num, channel** c, channel_value* v); + static int ola_handle(size_t num, managed_fd* fds); + static int ola_start(); + static int ola_shutdown(); +} + +#define MAP_COARSE 0x0200 +#define MAP_FINE 0x0400 +#define MAP_SINGLE 0x0800 +#define MAP_MARK 0x1000 +#define MAPPED_CHANNEL(a) ((a) & 0x01FF) +#define IS_ACTIVE(a) ((a) & 0xFE00) +#define IS_WIDE(a) ((a) & (MAP_FINE | MAP_COARSE)) +#define IS_SINGLE(a) ((a) & MAP_SINGLE) + +//since ola seems to immediately loop back any sent data as input, we only use one buffer +//to avoid excessive event feedback loops +typedef struct /*_ola_universe_model*/ { + uint8_t data[512]; + uint16_t map[512]; +} ola_universe; + +typedef struct /*_ola_instance_model*/ { + /*TODO does ola support remote connections?*/ + unsigned int universe_id; + ola_universe data; +} ola_instance_data; diff --git a/monster.cfg b/monster.cfg index 34acbce..d7c31dc 100644 --- a/monster.cfg +++ b/monster.cfg @@ -1,6 +1,3 @@ -[backend midi] -name = MIDIMonster - [backend sacn] name = sACN source bind = 0.0.0.0 @@ -8,27 +5,33 @@ bind = 0.0.0.0 [backend artnet] bind = 0.0.0.0 +[backend ola] + [artnet art] universe = 1 dest = 129.13.215.0 -[evdev in] -input = Xbox Wireless Controller - -[midi midi] +;[evdev in] +;input = Xbox Wireless Controller [sacn sacn] universe = 1 priority = 100 +[midi midi] + +[ola ola] + [map] -in.EV_ABS.ABS_X > midi.cc0.0 -in.EV_ABS.ABS_Y > midi.cc0.1 -in.EV_ABS.ABS_X > sacn.1+2 -in.EV_ABS.ABS_Y > sacn.3 -in.EV_ABS.ABS_X > art.1+2 -in.EV_ABS.ABS_Y > art.3 -in.EV_KEY.BTN_THUMBL > sacn.4 -in.EV_KEY.BTN_THUMBR > sacn.5 -in.EV_ABS.ABS_GAS > sacn.6+7 -in.EV_ABS.ABS_BRAKE > sacn.8 +;in.EV_ABS.ABS_X > sacn.1+2 +;in.EV_ABS.ABS_Y > sacn.3 +;in.EV_ABS.ABS_X > art.1+2 +;in.EV_ABS.ABS_Y > art.3 +;in.EV_ABS.ABS_X > ola.1+2 +;in.EV_ABS.ABS_Y > ola.3 +;in.EV_KEY.BTN_THUMBL > sacn.4 +;in.EV_KEY.BTN_THUMBR > sacn.5 +;in.EV_ABS.ABS_GAS > sacn.6+7 +;in.EV_ABS.ABS_BRAKE > sacn.8 +ola.1 > midi.cc0.1 +ola.2+3 > midi.cc0.2 -- cgit v1.2.3 From 90c58250ed8f10996000bc6cea10385f97969847 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 23 Mar 2019 14:30:11 +0100 Subject: Fix link visibilities --- backends/ola.cpp | 4 ++-- plugin.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backends/ola.cpp b/backends/ola.cpp index 299c883..c9c535c 100644 --- a/backends/ola.cpp +++ b/backends/ola.cpp @@ -153,7 +153,7 @@ static int ola_handle(size_t num, managed_fd* fds){ return 0; } -void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dmx, const std::string& error) { +static void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dmx, const std::string& error) { size_t p, max_mark = 0; //this should really be size_t but ola is weird... unsigned int dmx_length = 512; @@ -214,7 +214,7 @@ void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dmx, cons } } -void ola_register_callback(const std::string &error) { +static void ola_register_callback(const std::string &error) { if(!error.empty()){ fprintf(stderr, "OLA backend failed to register for universe: %s\n", error.c_str()); } diff --git a/plugin.c b/plugin.c index 48db410..fc642ac 100644 --- a/plugin.c +++ b/plugin.c @@ -8,8 +8,8 @@ #include #include "plugin.h" -size_t plugins = 0; -void** plugin_handle = NULL; +static size_t plugins = 0; +static void** plugin_handle = NULL; static int plugin_attach(char* path, char* file){ plugin_init init = NULL; -- cgit v1.2.3 From fa8f005a6c97480a671146c483cf1183d5f1b083 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 24 Mar 2019 15:20:25 +0100 Subject: Introduce full build target in root --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cda8fae..5ee9cd9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean run sanitize backends +.PHONY: all clean run sanitize backends full backends-full OBJS = config.o backend.o plugin.o PLUGINDIR = "\"./backends/\"" @@ -19,9 +19,14 @@ endif all: midimonster backends +full: midimonster backends-full + backends: $(MAKE) -C backends +backends-full: + $(MAKE) -C backends full + # This rule can not be the default rule because OSX the target prereqs are not exactly the build prereqs midimonster: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ -- cgit v1.2.3 From 1ed17293bedaf5bf5182d863cd406d7eb66b4501 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 24 Mar 2019 19:22:30 +0100 Subject: Factor out socket operations (Fixes #13) --- backends/Makefile | 14 ++++-- backends/artnet.c | 131 ++++++++---------------------------------------- backends/libmmbackend.c | 117 ++++++++++++++++++++++++++++++++++++++++++ backends/libmmbackend.h | 31 ++++++++++++ backends/osc.c | 123 +++------------------------------------------ backends/sacn.c | 131 ++++++++---------------------------------------- 6 files changed, 206 insertions(+), 341 deletions(-) create mode 100644 backends/libmmbackend.c create mode 100644 backends/libmmbackend.h diff --git a/backends/Makefile b/backends/Makefile index aef39c4..a7ea35a 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -2,6 +2,7 @@ OPTIONAL_BACKENDS = ola.so LINUX_BACKENDS = midi.so evdev.so BACKENDS = artnet.so osc.so loopback.so sacn.so +BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) @@ -18,6 +19,9 @@ ifeq ($(SYSTEM),Darwin) LDFLAGS += -undefined dynamic_lookup endif +artnet.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +osc.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB) midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) @@ -25,14 +29,14 @@ ola.so: LDLIBS = -lola ola.so: CPPFLAGS += -Wno-write-strings %.so :: %.c %.h - $(CC) $(CFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS) + $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) %.so :: %.cpp %.h - $(CXX) $(CPPFLAGS) $(LDLIBS) $< -o $@ $(LDFLAGS) + $(CXX) $(CPPFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) -all: $(BACKENDS) +all: $(BACKEND_LIB) $(BACKENDS) -full: $(BACKENDS) $(OPTIONAL_BACKENDS) +full: $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) clean: - $(RM) $(BACKENDS) $(OPTIONAL_BACKENDS) + $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) diff --git a/backends/artnet.c b/backends/artnet.c index d9ebfe5..8b404a6 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -1,12 +1,9 @@ #include -#include -#include -#include -#include -#include #include #include +#include "libmmbackend.h" + #include "artnet.h" #define MAX_FDS 255 #define BACKEND_NAME "artnet" @@ -16,68 +13,14 @@ static size_t artnet_fds = 0; static artnet_descriptor* artnet_fd = NULL; static int artnet_listener(char* host, char* port){ - int fd = -1, status, yes = 1, flags; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_DGRAM, - .ai_flags = AI_PASSIVE - }; - struct addrinfo* info; - struct addrinfo* addr_it; - + int fd; if(artnet_fds >= MAX_FDS){ fprintf(stderr, "ArtNet backend descriptor limit reached\n"); return -1; } - status = getaddrinfo(host, port, &hints, &info); - if(status){ - fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); - return -1; - } - - for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ - fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); - if(fd < 0){ - continue; - } - - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); - } - - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); - } - - yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); - } - - status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); - if(status < 0){ - close(fd); - continue; - } - - break; - } - - freeaddrinfo(info); - - if(!addr_it){ - fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); - return -1; - } - - //set nonblocking - flags = fcntl(fd, F_GETFL, 0); - if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ - fprintf(stderr, "Failed to set ArtNet descriptor nonblocking\n"); - close(fd); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + if(fd < 0){ return -1; } @@ -98,50 +41,6 @@ static int artnet_listener(char* host, char* port){ return 0; } -static int artnet_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ - struct addrinfo* head; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_DGRAM - }; - - int error = getaddrinfo(host, port, &hints, &head); - if(error || !head){ - fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); - return 1; - } - - memcpy(addr, head->ai_addr, head->ai_addrlen); - *len = head->ai_addrlen; - - freeaddrinfo(head); - return 0; -} - -static int artnet_separate_hostspec(char* in, char** host, char** port){ - size_t u; - - if(!in || !host || !port){ - return 1; - } - - for(u = 0; in[u] && !isspace(in[u]); u++){ - } - - //guess - *host = in; - - if(in[u]){ - in[u] = 0; - *port = in + u + 1; - } - else{ - //no port given - *port = ARTNET_PORT; - } - return 0; -} - int init(){ backend artnet = { .name = BACKEND_NAME, @@ -171,8 +70,14 @@ static int artnet_configure(char* option, char* value){ return 0; } else if(!strcmp(option, "bind")){ - if(artnet_separate_hostspec(value, &host, &port)){ - fprintf(stderr, "Not a valid ArtNet bind address: %s\n", value); + mmbackend_parse_hostspec(value, &host, &port); + + if(!port){ + port = ARTNET_PORT; + } + + if(!host){ + fprintf(stderr, "Not valid ArtNet bind address given\n"); return 1; } @@ -228,12 +133,18 @@ static int artnet_configure_instance(instance* inst, char* option, char* value){ return 0; } else if(!strcmp(option, "dest") || !strcmp(option, "destination")){ - if(artnet_separate_hostspec(value, &host, &port)){ + mmbackend_parse_hostspec(value, &host, &port); + + if(!port){ + port = ARTNET_PORT; + } + + if(!host){ fprintf(stderr, "Not a valid ArtNet destination for instance %s\n", inst->name); return 1; } - return artnet_parse_addr(host, port, &data->dest_addr, &data->dest_len); + return mmbackend_parse_sockaddr(host, port, &data->dest_addr, &data->dest_len); } fprintf(stderr, "Unknown ArtNet option %s for instance %s\n", option, inst->name); diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c new file mode 100644 index 0000000..6320611 --- /dev/null +++ b/backends/libmmbackend.c @@ -0,0 +1,117 @@ +#include "libmmbackend.h" + +void mmbackend_parse_hostspec(char* spec, char** host, char** port){ + size_t u = 0; + + if(!spec || !host || !port){ + return; + } + + *port = NULL; + + //skip leading spaces + for(; spec[u] && isspace(spec[u]); u++){ + } + + if(!spec[u]){ + *host = NULL; + return; + } + + *host = spec + u; + + //scan until string end or space + for(; spec[u] && !isspace(spec[u]); u++){ + } + + //if space, the rest should be the port + if(spec[u]){ + spec[u] = 0; + *port = spec + u + 1; + } +} + +int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ + struct addrinfo* head; + struct addrinfo hints = { + .ai_family = AF_UNSPEC + }; + + int error = getaddrinfo(host, port, &hints, &head); + if(error || !head){ + fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); + return 1; + } + + memcpy(addr, head->ai_addr, head->ai_addrlen); + if(len){ + *len = head->ai_addrlen; + } + + freeaddrinfo(head); + return 0; +} + +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ + int fd = -1, status, yes = 1, flags; + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = socktype, + .ai_flags = (listener ? AI_PASSIVE : 0) + }; + struct addrinfo *info, *addr_it; + + status = getaddrinfo(host, port, &hints, &info); + if(status){ + fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(status)); + return -1; + } + + //traverse the result list + for(addr_it = info; addr_it; addr_it = addr_it->ai_next){ + fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); + if(fd < 0){ + continue; + } + + //set required socket options + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to enable SO_REUSEADDR on socket\n"); + } + + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to enable SO_BROADCAST on socket\n"); + } + + yes = 0; + if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to disable IP_MULTICAST_LOOP on socket: %s\n", strerror(errno)); + } + + status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } + + break; + } + freeaddrinfo(info); + + if(!addr_it){ + fprintf(stderr, "Failed to create socket for %s port %s\n", host, port); + return -1; + } + + //set nonblocking + flags = fcntl(fd, F_GETFL, 0); + if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ + fprintf(stderr, "Failed to set socket nonblocking\n"); + close(fd); + return -1; + } + + return fd; +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h new file mode 100644 index 0000000..38bfca0 --- /dev/null +++ b/backends/libmmbackend.h @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Parse spec as host specification in the form + * host port + * into its constituent parts. + * Returns offsets into the original string and modifies it. + * Returns NULL in *port if none given. + * Returns NULL in both *port and *host if spec was an empty string. + */ +void mmbackend_parse_hostspec(char* spec, char** host, char** port); + +/* Parse a given host / port combination into a sockaddr_storage + * suitable for usage with connect / sendto + * Returns 0 on success + */ +int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len); + +/* Create a socket of given type and mode for a bind / connect host. + * Returns -1 on failure, a valid file descriptor for the socket on success. + */ +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener); diff --git a/backends/osc.c b/backends/osc.c index 5f94ec2..9996f68 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -1,9 +1,8 @@ #include -#include #include -#include #include -#include +#include "libmmbackend.h" + #include "osc.h" /* @@ -254,114 +253,6 @@ static int osc_validate_path(char* path){ return 0; } -static int osc_separate_hostspec(char* in, char** host, char** port){ - size_t u; - - if(!in || !host || !port){ - return 1; - } - - for(u = 0; in[u] && !isspace(in[u]); u++){ - } - - //guess - *host = in; - - if(in[u]){ - in[u] = 0; - *port = in + u + 1; - } - else{ - //no port given - *port = NULL; - } - return 0; -} - -static int osc_listener(char* host, char* port){ - int fd = -1, status, yes = 1, flags; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_DGRAM, - .ai_flags = AI_PASSIVE - }; - struct addrinfo* info; - struct addrinfo* addr_it; - - status = getaddrinfo(host, port, &hints, &info); - if(status){ - fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); - return -1; - } - - for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ - fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); - if(fd < 0){ - continue; - } - - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); - } - - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); - } - - yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); - } - - status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); - if(status < 0){ - close(fd); - continue; - } - - break; - } - - freeaddrinfo(info); - - if(!addr_it){ - fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); - return -1; - } - - //set nonblocking - flags = fcntl(fd, F_GETFL, 0); - if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ - close(fd); - fprintf(stderr, "Failed to set OSC descriptor nonblocking\n"); - return -1; - } - - return fd; -} - -static int osc_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ - struct addrinfo* head; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_DGRAM - }; - - int error = getaddrinfo(host, port, &hints, &head); - if(error || !head){ - fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); - return 1; - } - - memcpy(addr, head->ai_addr, head->ai_addrlen); - *len = head->ai_addrlen; - - freeaddrinfo(head); - return 0; -} - static int backend_configure(char* option, char* value){ fprintf(stderr, "The OSC backend does not take any global configuration\n"); return 1; @@ -390,12 +281,13 @@ static int backend_configure_instance(instance* inst, char* option, char* value) return 0; } else if(!strcmp(option, "bind")){ - if(osc_separate_hostspec(value, &host, &port)){ + mmbackend_parse_hostspec(value, &host, &port); + if(!host || !port){ fprintf(stderr, "Invalid bind address for instance %s\n", inst->name); return 1; } - data->fd = osc_listener(host, port); + data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); if(data->fd < 0){ fprintf(stderr, "Failed to bind for instance %s\n", inst->name); return 1; @@ -413,12 +305,13 @@ static int backend_configure_instance(instance* inst, char* option, char* value) return 0; } - if(osc_separate_hostspec(value, &host, &port)){ + mmbackend_parse_hostspec(value, &host, &port); + if(!host || !port){ fprintf(stderr, "Invalid destination address for instance %s\n", inst->name); return 1; } - if(osc_parse_addr(host, port, &data->dest, &data->dest_len)){ + if(mmbackend_parse_sockaddr(host, port, &data->dest, &data->dest_len)){ fprintf(stderr, "Failed to parse destination address for instance %s\n", inst->name); return 1; } diff --git a/backends/sacn.c b/backends/sacn.c index fde8d90..75bb76f 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -8,6 +8,8 @@ #include #include +#include "libmmbackend.h" + #include "sacn.h" //upper limit imposed by using the fd index as 16-bit part of the instance id #define MAX_FDS 4096 @@ -50,68 +52,14 @@ int init(){ } static int sacn_listener(char* host, char* port, uint8_t fd_flags){ - int fd = -1, status, yes = 1, flags; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_DGRAM, - .ai_flags = AI_PASSIVE - }; - struct addrinfo* info; - struct addrinfo* addr_it; - + int fd = -1; if(global_cfg.fds >= MAX_FDS){ fprintf(stderr, "sACN backend descriptor limit reached\n"); return -1; } - status = getaddrinfo(host, port, &hints, &info); - if(status){ - fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); - return -1; - } - - for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ - fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); - if(fd < 0){ - continue; - } - - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); - } - - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); - } - - yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); - } - - status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); - if(status < 0){ - close(fd); - continue; - } - - break; - } - - freeaddrinfo(info); - - if(!addr_it){ - fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); - return -1; - } - - //set nonblocking - flags = fcntl(fd, F_GETFL, 0); - if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ - fprintf(stderr, "Failed to set sACN descriptor nonblocking\n"); - close(fd); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + if(fd < 0){ return -1; } @@ -133,55 +81,6 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){ return 0; } -static int sacn_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ - struct addrinfo* head; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_DGRAM - }; - - int error = getaddrinfo(host, port, &hints, &head); - if(error || !head){ - fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); - return 1; - } - - memcpy(addr, head->ai_addr, head->ai_addrlen); - *len = head->ai_addrlen; - - freeaddrinfo(head); - return 0; -} - -static int sacn_parse_hostspec(char* in, char** host, char** port, uint8_t* flags){ - size_t u; - - if(!in || !host || !port){ - return 1; - } - - for(u = 0; in[u] && !isspace(in[u]); u++){ - } - - //guess - *host = in; - - if(in[u]){ - in[u] = 0; - *port = in + u + 1; - } - else{ - //no port given - *port = SACN_PORT; - } - - if(flags){ - //TODO parse hostspec trailing data for options - *flags = 0; - } - return 0; -} - static int sacn_configure(char* option, char* value){ char* host = NULL, *port = NULL, *next = NULL; uint8_t flags = 0; @@ -204,8 +103,13 @@ static int sacn_configure(char* option, char* value){ } } else if(!strcmp(option, "bind")){ - if(sacn_parse_hostspec(value, &host, &port, &flags)){ - fprintf(stderr, "Not a valid sACN bind address: %s\n", value); + mmbackend_parse_hostspec(value, &host, &port); + if(!port){ + port = SACN_PORT; + } + + if(!host){ + fprintf(stderr, "No valid sACN bind address provided\n"); return 1; } @@ -243,12 +147,17 @@ static int sacn_configure_instance(instance* inst, char* option, char* value){ return 0; } else if(!strcmp(option, "destination")){ - if(sacn_parse_hostspec(value, &host, &port, NULL)){ - fprintf(stderr, "Not a valid sACN destination for instance %s: %s\n", inst->name, value); + mmbackend_parse_hostspec(value, &host, &port); + if(!port){ + port = SACN_PORT; + } + + if(!host){ + fprintf(stderr, "No valid sACN destination for instance %s\n", inst->name); return 1; } - return sacn_parse_addr(host, port, &data->dest_addr, &data->dest_len); + return mmbackend_parse_sockaddr(host, port, &data->dest_addr, &data->dest_len); } else if(!strcmp(option, "from")){ next = value; -- cgit v1.2.3 From 3949f80417f0361e79f22658a8b900b228f125f1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 24 Mar 2019 19:31:01 +0100 Subject: Fix minor issues reported by Coverity Scan --- backends/ola.cpp | 2 +- midimonster.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/ola.cpp b/backends/ola.cpp index c9c535c..632cef7 100644 --- a/backends/ola.cpp +++ b/backends/ola.cpp @@ -242,7 +242,7 @@ static int ola_start(){ ola_select->AddReadDescriptor(ola_socket); - fprintf(stderr, "OLA backend registering %zu descriptors to core\n", 1); + fprintf(stderr, "OLA backend registering connection descriptor to core\n"); if(mm_manage_fd(ola_socket->ReadDescriptor(), BACKEND_NAME, 1, NULL)){ goto bail; } diff --git a/midimonster.c b/midimonster.c index 00d11f3..043f9e2 100644 --- a/midimonster.c +++ b/midimonster.c @@ -244,7 +244,7 @@ static fd_set fds_collect(int* max_fd){ } int main(int argc, char** argv){ - fd_set all_fds, read_fds; + fd_set all_fds = {}, read_fds; event_collection* secondary = NULL; struct timeval tv; size_t u, n; -- cgit v1.2.3 From cf76f45ec229ac8f6f6fdbd3834c139f28c2ea57 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 24 Mar 2019 19:34:08 +0100 Subject: Don't use GNU extensions for initializers... --- midimonster.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/midimonster.c b/midimonster.c index 043f9e2..4784052 100644 --- a/midimonster.c +++ b/midimonster.c @@ -244,7 +244,7 @@ static fd_set fds_collect(int* max_fd){ } int main(int argc, char** argv){ - fd_set all_fds = {}, read_fds; + fd_set all_fds, read_fds; event_collection* secondary = NULL; struct timeval tv; size_t u, n; @@ -255,6 +255,7 @@ int main(int argc, char** argv){ cfg_file = argv[1]; } + FD_ZERO(&all_fds); //initialize backends if(plugins_load(PLUGINS)){ fprintf(stderr, "Failed to initialize a backend\n"); -- cgit v1.2.3 From 6e8f195c36cb5f5cd6469658937336f2d31ab6e8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 24 Mar 2019 19:57:45 +0100 Subject: Hack-fix parallel builds --- backends/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/Makefile b/backends/Makefile index a7ea35a..c11de56 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -28,7 +28,7 @@ evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) ola.so: LDLIBS = -lola ola.so: CPPFLAGS += -Wno-write-strings -%.so :: %.c %.h +%.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) %.so :: %.cpp %.h -- cgit v1.2.3 From 7086cf6514462512ffcca6cb72876bd76b193cab Mon Sep 17 00:00:00 2001 From: Peter Newman Date: Tue, 26 Mar 2019 00:31:29 +0000 Subject: Build OLA plugin too --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1b7c2e2..7ad9dc3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,7 @@ addons: - *base_build - libasound2-dev - libevdev-dev + - libola-dev packages: &core_build_gpp_latest - *core_build - gcc-8 -- cgit v1.2.3 From c7593145d9d71733ea6a0b32bcd52dd21b3242b8 Mon Sep 17 00:00:00 2001 From: Peter Newman Date: Tue, 26 Mar 2019 00:32:23 +0000 Subject: Actually build the OLA plugin --- .travis-ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index b3e0744..ddfa93d 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -74,6 +74,6 @@ elif [[ $TASK = 'sanitize' ]]; then else # Otherwise compile as normal travis_fold start "make" - make; + make full; travis_fold end "make" fi -- cgit v1.2.3 From d4663f39bf3272316d5c9e8480e981f54d4f48fb Mon Sep 17 00:00:00 2001 From: Peter Newman Date: Tue, 26 Mar 2019 00:44:19 +0000 Subject: Install the ola libs on Mac, use the latest C++ compiler too --- .travis.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7ad9dc3..e01cb2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -157,14 +157,15 @@ install: before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then PATH=/usr/local/opt/ccache/libexec:$PATH; fi # Use ccache on Mac too #Coverity doesn't work with g++ 5 or 6, so only upgrade to g++ 4.9 for that - - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) -a "$CC" = "gcc" ]; then export CC="ccache gcc-8"; fi + - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) -a "$CC" = "gcc" ]; then export CC="ccache gcc-8"; export CXX="ccache g++-8"; fi #Use the latest clang if we're compiling with clang - - if [ "$TRAVIS_OS_NAME" == "linux" -a "$CC" = "clang" ]; then export CC="clang-6.0"; fi -#Report the compiler version + - if [ "$TRAVIS_OS_NAME" == "linux" -a "$CC" = "clang" ]; then export CC="clang-6.0"; export CXX="clang-6.0"; fi +#Report the compiler versions - $CC --version + - $CXX --version - if [ "$TASK" == "spellintian" -o "$TASK" == "spellintian-duplicates" ]; then wget "http://archive.ubuntu.com/ubuntu/pool/main/l/lintian/lintian_2.5.104_all.deb"; sudo dpkg -i lintian_*.deb; sudo apt-get install -f -y; fi # Install a later lintian after_script: -- cgit v1.2.3 From 558a4b135cfbc9e72648cfbbc402dde22a36d632 Mon Sep 17 00:00:00 2001 From: Peter Newman Date: Tue, 26 Mar 2019 01:02:43 +0000 Subject: Install g++8 too, for the C++ compiles --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e01cb2a..df4a97a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,7 @@ addons: packages: &core_build_gpp_latest - *core_build - gcc-8 + - g++-8 packages: &core_build_clang_latest - *core_build - clang-6.0 @@ -165,7 +166,8 @@ before_install: - if [ "$TRAVIS_OS_NAME" == "linux" -a "$CC" = "clang" ]; then export CC="clang-6.0"; export CXX="clang-6.0"; fi #Report the compiler versions - $CC --version - - $CXX --version +#OS X uses something other than $CXX variable + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then $CXX --version; fi - if [ "$TASK" == "spellintian" -o "$TASK" == "spellintian-duplicates" ]; then wget "http://archive.ubuntu.com/ubuntu/pool/main/l/lintian/lintian_2.5.104_all.deb"; sudo dpkg -i lintian_*.deb; sudo apt-get install -f -y; fi # Install a later lintian after_script: -- cgit v1.2.3 From d682a8df9a151538d70c8a7a23a68636284b1b94 Mon Sep 17 00:00:00 2001 From: Peter Newman Date: Tue, 26 Mar 2019 01:18:38 +0000 Subject: Only run $CXX version when necessary --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index df4a97a..ab047ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -167,7 +167,7 @@ before_install: #Report the compiler versions - $CC --version #OS X uses something other than $CXX variable - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then $CXX --version; fi + - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) ]; then $CXX --version; fi - if [ "$TASK" == "spellintian" -o "$TASK" == "spellintian-duplicates" ]; then wget "http://archive.ubuntu.com/ubuntu/pool/main/l/lintian/lintian_2.5.104_all.deb"; sudo dpkg -i lintian_*.deb; sudo apt-get install -f -y; fi # Install a later lintian after_script: -- cgit v1.2.3 From a2b0728027dd8961ef84220c8c8eaf8a81154c71 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 30 Mar 2019 14:34:20 +0100 Subject: Fix MIDI mapping syntax --- README.md | 17 ++- backends/midi.c | 35 +++++- configs/launchctl-sacn.cfg | 48 ++++---- configs/midi-osc.cfg | 32 ++--- configs/osc-kbd.cfg | 24 ++-- configs/unifest-17.cfg | 288 ++++++++++++++++++++++----------------------- 6 files changed, 239 insertions(+), 205 deletions(-) diff --git a/README.md b/README.md index 3f5606a..8dfcdd8 100644 --- a/README.md +++ b/README.md @@ -245,11 +245,19 @@ The MIDI backend supports multiple channel types * `note` - Note On/Off messages * `nrpn` - NRPNs (not yet implemented) -A channel is specified using `.`. +A channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead +of `channel`. +The earlier syntax of `.` is officially deprecated but still supported for compatability +reasons. This support may be removed at some future time. -Example mapping: +Channels range from `0` to `15`. Each channel consists of 128 notes (numbered `0` through `127`) and 128 CC's +(numbered likewise), a channel pressure control (also called 'channel aftertouch') and a pitch control. +Each Note also has an additional pressure value associated with it. + +Example mappings: ``` -midi1.cc0.9 > midi2.note1.4 +midi1.ch0.note9 > midi2.channel1.cc4 +midi1.channel15.cc1 > midi1.channel0.note0 ``` #### Known bugs / problems @@ -259,7 +267,8 @@ a configuration option at a later time. NRPNs are not yet fully implemented, though rudimentary support is in the codebase. -The channel specification syntax is currently a bit clunky. +To see which events your MIDI devices output, ALSA provides the `aseqdump` utility. You can +list all incoming events using `aseqdump -p `. ### The `evdev` backend diff --git a/backends/midi.c b/backends/midi.c index d856ced..ad7f6fe 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -118,19 +118,30 @@ static channel* midi_channel(instance* instance, char* spec){ .label = 0 }; + //support deprecated syntax for a transition period... + uint8_t old_syntax = 0; char* channel; - if(!strncmp(spec, "cc", 2)){ + if(!strncmp(spec, "ch", 2)){ + channel = spec + 2; + if(!strncmp(spec, "channel", 7)){ + channel = spec + 7; + } + } + else if(!strncmp(spec, "cc", 2)){ ident.fields.type = cc; channel = spec + 2; + old_syntax = 1; } else if(!strncmp(spec, "note", 4)){ ident.fields.type = note; channel = spec + 4; + old_syntax = 1; } else if(!strncmp(spec, "nrpn", 4)){ ident.fields.type = nrpn; channel = spec + 4; + old_syntax = 1; } else{ fprintf(stderr, "Unknown MIDI channel specification %s\n", spec); @@ -138,19 +149,33 @@ static channel* midi_channel(instance* instance, char* spec){ } ident.fields.channel = strtoul(channel, &channel, 10); - - //FIXME test this - if(ident.fields.channel > 16){ + if(ident.fields.channel > 15){ fprintf(stderr, "MIDI channel out of range in channel spec %s\n", spec); return NULL; } if(*channel != '.'){ - fprintf(stderr, "Need MIDI channel specification of form channel.control, had %s\n", spec); + fprintf(stderr, "Need MIDI channel specification of form channel., had %s\n", spec); return NULL; } + //skip the period channel++; + if(!old_syntax){ + if(!strncmp(channel, "cc", 2)){ + ident.fields.type = cc; + channel += 2; + } + else if(!strncmp(channel, "note", 4)){ + ident.fields.type = note; + channel += 4; + } + else if(!strncmp(channel, "nrpn", 4)){ + ident.fields.type = nrpn; + channel += 4; + } + } + ident.fields.control = strtoul(channel, NULL, 10); if(ident.label){ diff --git a/configs/launchctl-sacn.cfg b/configs/launchctl-sacn.cfg index 164b477..c2dec84 100644 --- a/configs/launchctl-sacn.cfg +++ b/configs/launchctl-sacn.cfg @@ -18,28 +18,28 @@ priority = 100 [map] -lc.cc0.0 > out.1 -lc.cc0.1 > out.2 -lc.cc0.2 > out.3 -lc.cc0.3 > out.4 -lc.cc0.4 > out.5 -lc.cc0.5 > out.6 -lc.cc0.6 > out.7 -lc.cc0.7 > out.8 -lc.cc0.8 > out.9 -lc.cc0.9 > out.10 -lc.cc0.10 > out.11 -lc.cc0.11 > out.12 -lc.cc0.12 > out.13 -lc.cc0.13 > out.14 -lc.cc0.14 > out.15 -lc.cc0.15 > out.16 +lc.ch0.cc0 > out.1 +lc.ch0.cc1 > out.2 +lc.ch0.cc2 > out.3 +lc.ch0.cc3 > out.4 +lc.ch0.cc4 > out.5 +lc.ch0.cc5 > out.6 +lc.ch0.cc6 > out.7 +lc.ch0.cc7 > out.8 +lc.ch0.cc8 > out.9 +lc.ch0.cc9 > out.10 +lc.ch0.cc10 > out.11 +lc.ch0.cc11 > out.12 +lc.ch0.cc12 > out.13 +lc.ch0.cc13 > out.14 +lc.ch0.cc14 > out.15 +lc.ch0.cc15 > out.16 -lc.note0.0 > out.1 -lc.note0.1 > out.2 -lc.note0.2 > out.3 -lc.note0.3 > out.4 -lc.note0.4 > out.5 -lc.note0.5 > out.6 -lc.note0.6 > out.7 -lc.note0.7 > out.8 +lc.ch0.note0 > out.1 +lc.ch0.note1 > out.2 +lc.ch0.note2 > out.3 +lc.ch0.note3 > out.4 +lc.ch0.note4 > out.5 +lc.ch0.note5 > out.6 +lc.ch0.note6 > out.7 +lc.ch0.note7 > out.8 diff --git a/configs/midi-osc.cfg b/configs/midi-osc.cfg index 215daa9..755077c 100644 --- a/configs/midi-osc.cfg +++ b/configs/midi-osc.cfg @@ -33,20 +33,20 @@ write = BCF [map] -bcf.cc0.81 <> touch./fader1 -bcf.cc0.82 <> touch./fader2 -bcf.cc0.83 <> touch./fader3 -bcf.cc0.84 <> touch./fader4 -bcf.cc0.85 <> touch./fader5 -bcf.cc0.86 <> touch./fader6 -bcf.cc0.87 <> touch./fader7 -bcf.cc0.88 <> touch./fader8 +bcf.ch0.cc81 <> touch./fader1 +bcf.ch0.cc82 <> touch./fader2 +bcf.ch0.cc83 <> touch./fader3 +bcf.ch0.cc84 <> touch./fader4 +bcf.ch0.cc85 <> touch./fader5 +bcf.ch0.cc86 <> touch./fader6 +bcf.ch0.cc87 <> touch./fader7 +bcf.ch0.cc88 <> touch./fader8 -bcf.cc0.81 <> touch./multifader1/1 -bcf.cc0.82 <> touch./multifader1/2 -bcf.cc0.83 <> touch./multifader1/3 -bcf.cc0.84 <> touch./multifader1/4 -bcf.cc0.85 <> touch./multifader1/5 -bcf.cc0.86 <> touch./multifader1/6 -bcf.cc0.87 <> touch./multifader1/7 -bcf.cc0.88 <> touch./multifader1/8 +bcf.ch0.cc81 <> touch./multifader1/1 +bcf.ch0.cc82 <> touch./multifader1/2 +bcf.ch0.cc83 <> touch./multifader1/3 +bcf.ch0.cc84 <> touch./multifader1/4 +bcf.ch0.cc85 <> touch./multifader1/5 +bcf.ch0.cc86 <> touch./multifader1/6 +bcf.ch0.cc87 <> touch./multifader1/7 +bcf.ch0.cc88 <> touch./multifader1/8 diff --git a/configs/osc-kbd.cfg b/configs/osc-kbd.cfg index 0abd131..eb80378 100644 --- a/configs/osc-kbd.cfg +++ b/configs/osc-kbd.cfg @@ -11,15 +11,15 @@ bind = * 8000 dest = learn@8001 [map] -pad./1/push1 > out.note0.60 -pad./1/push2 > out.note0.61 -pad./1/push3 > out.note0.62 -pad./1/push4 > out.note0.63 -pad./1/push5 > out.note0.64 -pad./1/push6 > out.note0.65 -pad./1/push7 > out.note0.66 -pad./1/push8 > out.note0.67 -pad./1/push9 > out.note0.68 -pad./1/push10 > out.note0.69 -pad./1/push11 > out.note0.70 -pad./1/push12 > out.note0.71 +pad./1/push1 > out.ch0.note60 +pad./1/push2 > out.ch0.note61 +pad./1/push3 > out.ch0.note62 +pad./1/push4 > out.ch0.note63 +pad./1/push5 > out.ch0.note64 +pad./1/push6 > out.ch0.note65 +pad./1/push7 > out.ch0.note66 +pad./1/push8 > out.ch0.note67 +pad./1/push9 > out.ch0.note68 +pad./1/push10 > out.ch0.note69 +pad./1/push11 > out.ch0.note70 +pad./1/push12 > out.ch0.note71 diff --git a/configs/unifest-17.cfg b/configs/unifest-17.cfg index 1fab484..47e9ec2 100644 --- a/configs/unifest-17.cfg +++ b/configs/unifest-17.cfg @@ -40,156 +40,156 @@ read = 36:1 [map] ; ArtNet -claudius.1 < lc1.cc0.1 -claudius.2 < lc1.cc0.2 -claudius.3 < lc1.cc0.3 -claudius.4 < lc1.cc0.4 -claudius.5 < lc1.cc0.5 -claudius.6 < lc1.cc0.6 -claudius.7 < lc1.cc0.7 -claudius.8 < lc1.cc0.8 -claudius.9 < lc1.cc0.9 -claudius.10 < lc1.cc0.10 -claudius.11 < lc1.cc0.11 -claudius.12 < lc1.cc0.12 -claudius.13 < lc1.cc0.13 -claudius.14 < lc1.cc0.14 -claudius.15 < lc1.cc0.15 -claudius.16 < lc1.cc0.16 +claudius.1 < lc1.ch0.cc1 +claudius.2 < lc1.ch0.cc2 +claudius.3 < lc1.ch0.cc3 +claudius.4 < lc1.ch0.cc4 +claudius.5 < lc1.ch0.cc5 +claudius.6 < lc1.ch0.cc6 +claudius.7 < lc1.ch0.cc7 +claudius.8 < lc1.ch0.cc8 +claudius.9 < lc1.ch0.cc9 +claudius.10 < lc1.ch0.cc10 +claudius.11 < lc1.ch0.cc11 +claudius.12 < lc1.ch0.cc12 +claudius.13 < lc1.ch0.cc13 +claudius.14 < lc1.ch0.cc14 +claudius.15 < lc1.ch0.cc15 +claudius.16 < lc1.ch0.cc16 ; BCF Fader -out.note0.0 < bcf1.cc0.81 -out.note0.1 < bcf1.cc0.82 -out.note0.2 < bcf1.cc0.83 -out.note0.3 < bcf1.cc0.84 -out.note0.4 < bcf1.cc0.85 -out.note0.5 < bcf1.cc0.86 -out.note0.6 < bcf1.cc0.87 -out.note0.7 < bcf1.cc0.88 -out.note0.8 < bcf2.cc0.81 -out.note0.9 < bcf2.cc0.82 -out.note0.10 < bcf2.cc0.83 -out.note0.11 < bcf2.cc0.84 -out.note0.12 < bcf2.cc0.85 -out.note0.13 < bcf2.cc0.86 -out.note0.14 < bcf2.cc0.87 -out.note0.15 < bcf2.cc0.88 +out.ch0.ch0.note< bcf1.ch0.cc81 +out.ch0.note1 < bcf1.ch0.cc82 +out.ch0.note2 < bcf1.ch0.cc83 +out.ch0.note3 < bcf1.ch0.cc84 +out.ch0.note4 < bcf1.ch0.cc85 +out.ch0.note5 < bcf1.ch0.cc86 +out.ch0.note6 < bcf1.ch0.cc87 +out.ch0.note7 < bcf1.ch0.cc88 +out.ch0.note8 < bcf2.ch0.cc81 +out.ch0.note9 < bcf2.ch0.cc82 +out.ch0.note10 < bcf2.ch0.cc83 +out.ch0.note11 < bcf2.ch0.cc84 +out.ch0.note12 < bcf2.ch0.cc85 +out.ch0.note13 < bcf2.ch0.cc86 +out.ch0.note14 < bcf2.ch0.cc87 +out.ch0.note15 < bcf2.ch0.cc88 ; LC Rotary -out.note0.16 < lc1.cc0.1 -out.note0.17 < lc1.cc0.2 -out.note0.18 < lc1.cc0.3 -out.note0.19 < lc1.cc0.4 -out.note0.20 < lc1.cc0.5 -out.note0.21 < lc1.cc0.6 -out.note0.22 < lc1.cc0.7 -out.note0.23 < lc1.cc0.8 -out.note0.24 < lc1.cc0.9 -out.note0.25 < lc1.cc0.10 -out.note0.26 < lc1.cc0.11 -out.note0.27 < lc1.cc0.12 -out.note0.28 < lc1.cc0.13 -out.note0.29 < lc1.cc0.14 -out.note0.30 < lc1.cc0.15 -out.note0.31 < lc1.cc0.16 -out.note0.32 < lc2.cc0.1 -out.note0.33 < lc2.cc0.2 -out.note0.34 < lc2.cc0.3 -out.note0.35 < lc2.cc0.4 -out.note0.36 < lc2.cc0.5 -out.note0.37 < lc2.cc0.6 -out.note0.38 < lc2.cc0.7 -out.note0.39 < lc2.cc0.8 -out.note0.40 < lc2.cc0.9 -out.note0.41 < lc2.cc0.10 -out.note0.42 < lc2.cc0.11 -out.note0.43 < lc2.cc0.12 -out.note0.44 < lc2.cc0.13 -out.note0.45 < lc2.cc0.14 -out.note0.46 < lc2.cc0.15 -out.note0.47 < lc2.cc0.16 +out.ch0.note16 < lc1.ch0.cc1 +out.ch0.note17 < lc1.ch0.cc2 +out.ch0.note18 < lc1.ch0.cc3 +out.ch0.note19 < lc1.ch0.cc4 +out.ch0.note20 < lc1.ch0.cc5 +out.ch0.note21 < lc1.ch0.cc6 +out.ch0.note22 < lc1.ch0.cc7 +out.ch0.note23 < lc1.ch0.cc8 +out.ch0.note24 < lc1.ch0.cc9 +out.ch0.note25 < lc1.ch0.cc10 +out.ch0.note26 < lc1.ch0.cc11 +out.ch0.note27 < lc1.ch0.cc12 +out.ch0.note28 < lc1.ch0.cc13 +out.ch0.note29 < lc1.ch0.cc14 +out.ch0.note30 < lc1.ch0.cc15 +out.ch0.note31 < lc1.ch0.cc16 +out.ch0.note32 < lc2.ch0.cc1 +out.ch0.note33 < lc2.ch0.cc2 +out.ch0.note34 < lc2.ch0.cc3 +out.ch0.note35 < lc2.ch0.cc4 +out.ch0.note36 < lc2.ch0.cc5 +out.ch0.note37 < lc2.ch0.cc6 +out.ch0.note38 < lc2.ch0.cc7 +out.ch0.note39 < lc2.ch0.cc8 +out.ch0.note40 < lc2.ch0.cc9 +out.ch0.note41 < lc2.ch0.cc10 +out.ch0.note42 < lc2.ch0.cc11 +out.ch0.note43 < lc2.ch0.cc12 +out.ch0.note44 < lc2.ch0.cc13 +out.ch0.note45 < lc2.ch0.cc14 +out.ch0.note46 < lc2.ch0.cc15 +out.ch0.note47 < lc2.ch0.cc16 ; LC Button -out.note0.48 < lc1.note0.0 -out.note0.49 < lc1.note0.1 -out.note0.50 < lc1.note0.2 -out.note0.51 < lc1.note0.3 -out.note0.52 < lc1.note0.4 -out.note0.53 < lc1.note0.5 -out.note0.54 < lc1.note0.6 -out.note0.55 < lc1.note0.7 +out.ch0.note48 < lc1.ch0.note0 +out.ch0.note49 < lc1.ch0.note1 +out.ch0.note50 < lc1.ch0.note2 +out.ch0.note51 < lc1.ch0.note3 +out.ch0.note52 < lc1.ch0.note4 +out.ch0.note53 < lc1.ch0.note5 +out.ch0.note54 < lc1.ch0.note6 +out.ch0.note55 < lc1.ch0.note7 -out.note0.56 < lc2.note0.0 -out.note0.57 < lc2.note0.1 -out.note0.58 < lc2.note0.2 -out.note0.59 < lc2.note0.3 -out.note0.60 < lc2.note0.4 -out.note0.61 < lc2.note0.5 -out.note0.62 < lc2.note0.6 -out.note0.63 < lc2.note0.7 +out.ch0.note56 < lc2.ch0.note0 +out.ch0.note57 < lc2.ch0.note1 +out.ch0.note58 < lc2.ch0.note2 +out.ch0.note59 < lc2.ch0.note3 +out.ch0.note60 < lc2.ch0.note4 +out.ch0.note61 < lc2.ch0.note5 +out.ch0.note62 < lc2.ch0.note6 +out.ch0.note63 < lc2.ch0.note7 ; Launchpad -out.note0.64 < pad.note0.0 -out.note0.65 < pad.note0.1 -out.note0.66 < pad.note0.2 -out.note0.67 < pad.note0.3 -out.note0.68 < pad.note0.4 -out.note0.69 < pad.note0.5 -out.note0.70 < pad.note0.6 -out.note0.71 < pad.note0.7 -out.note0.72 < pad.note0.16 -out.note0.73 < pad.note0.17 -out.note0.74 < pad.note0.18 -out.note0.75 < pad.note0.19 -out.note0.76 < pad.note0.20 -out.note0.77 < pad.note0.21 -out.note0.78 < pad.note0.22 -out.note0.79 < pad.note0.23 -out.note0.80 < pad.note0.32 -out.note0.81 < pad.note0.33 -out.note0.82 < pad.note0.34 -out.note0.83 < pad.note0.35 -out.note0.84 < pad.note0.36 -out.note0.85 < pad.note0.37 -out.note0.86 < pad.note0.38 -out.note0.87 < pad.note0.39 -out.note0.88 < pad.note0.48 -out.note0.89 < pad.note0.49 -out.note0.90 < pad.note0.50 -out.note0.91 < pad.note0.51 -out.note0.92 < pad.note0.52 -out.note0.93 < pad.note0.53 -out.note0.94 < pad.note0.54 -out.note0.95 < pad.note0.55 -out.note0.96 < pad.note0.64 -out.note0.97 < pad.note0.65 -out.note0.98 < pad.note0.66 -out.note0.99 < pad.note0.67 -out.note0.100 < pad.note0.68 -out.note0.101 < pad.note0.69 -out.note0.102 < pad.note0.70 -out.note0.103 < pad.note0.71 -out.note0.104 < pad.note0.80 -out.note0.105 < pad.note0.81 -out.note0.106 < pad.note0.82 -out.note0.107 < pad.note0.83 -out.note0.108 < pad.note0.84 -out.note0.109 < pad.note0.85 -out.note0.110 < pad.note0.86 -out.note0.111 < pad.note0.87 -out.note0.112 < pad.note0.96 -out.note0.113 < pad.note0.97 -out.note0.114 < pad.note0.98 -out.note0.115 < pad.note0.99 -out.note0.116 < pad.note0.100 -out.note0.117 < pad.note0.101 -out.note0.118 < pad.note0.102 -out.note0.119 < pad.note0.103 -out.note0.120 < pad.note0.112 -out.note0.121 < pad.note0.113 -out.note0.122 < pad.note0.114 -out.note0.123 < pad.note0.115 -out.note0.124 < pad.note0.116 -out.note0.125 < pad.note0.117 -out.note0.126 < pad.note0.118 -out.note0.127 < pad.note0.119 +out.ch0.note64 < pad.ch0.note0 +out.ch0.note65 < pad.ch0.note1 +out.ch0.note66 < pad.ch0.note2 +out.ch0.note67 < pad.ch0.note3 +out.ch0.note68 < pad.ch0.note4 +out.ch0.note69 < pad.ch0.note5 +out.ch0.note70 < pad.ch0.note6 +out.ch0.note71 < pad.ch0.note7 +out.ch0.note72 < pad.ch0.note16 +out.ch0.note73 < pad.ch0.note17 +out.ch0.note74 < pad.ch0.note18 +out.ch0.note75 < pad.ch0.note19 +out.ch0.note76 < pad.ch0.note20 +out.ch0.note77 < pad.ch0.note21 +out.ch0.note78 < pad.ch0.note22 +out.ch0.note79 < pad.ch0.note23 +out.ch0.note80 < pad.ch0.note32 +out.ch0.note81 < pad.ch0.note33 +out.ch0.note82 < pad.ch0.note34 +out.ch0.note83 < pad.ch0.note35 +out.ch0.note84 < pad.ch0.note36 +out.ch0.note85 < pad.ch0.note37 +out.ch0.note86 < pad.ch0.note38 +out.ch0.note87 < pad.ch0.note39 +out.ch0.note88 < pad.ch0.note48 +out.ch0.note89 < pad.ch0.note49 +out.ch0.note90 < pad.ch0.note50 +out.ch0.note91 < pad.ch0.note51 +out.ch0.note92 < pad.ch0.note52 +out.ch0.note93 < pad.ch0.note53 +out.ch0.note94 < pad.ch0.note54 +out.ch0.note95 < pad.ch0.note55 +out.ch0.note96 < pad.ch0.note64 +out.ch0.note97 < pad.ch0.note65 +out.ch0.note98 < pad.ch0.note66 +out.ch0.note99 < pad.ch0.note67 +out.ch0.note100 < pad.ch0.note68 +out.ch0.note101 < pad.ch0.note69 +out.ch0.note102 < pad.ch0.note70 +out.ch0.note103 < pad.ch0.note71 +out.ch0.note104 < pad.ch0.note80 +out.ch0.note105 < pad.ch0.note81 +out.ch0.note106 < pad.ch0.note82 +out.ch0.note107 < pad.ch0.note83 +out.ch0.note108 < pad.ch0.note84 +out.ch0.note109 < pad.ch0.note85 +out.ch0.note110 < pad.ch0.note86 +out.ch0.note111 < pad.ch0.note87 +out.ch0.note112 < pad.ch0.note96 +out.ch0.note113 < pad.ch0.note97 +out.ch0.note114 < pad.ch0.note98 +out.ch0.note115 < pad.ch0.note99 +out.ch0.note116 < pad.ch0.note100 +out.ch0.note117 < pad.ch0.note101 +out.ch0.note118 < pad.ch0.note102 +out.ch0.note119 < pad.ch0.note103 +out.ch0.note120 < pad.ch0.note112 +out.ch0.note121 < pad.ch0.note113 +out.ch0.note122 < pad.ch0.note114 +out.ch0.note123 < pad.ch0.note115 +out.ch0.note124 < pad.ch0.note116 +out.ch0.note125 < pad.ch0.note117 +out.ch0.note126 < pad.ch0.note118 +out.ch0.note127 < pad.ch0.note119 -- cgit v1.2.3 From 0c333567f599206cb0be6b74f02e59820536e0b2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 31 Mar 2019 09:00:12 +0200 Subject: Move backend documentation out of main README --- README.md | 429 ++------------------------------------------------- backends/artnet.md | 41 +++++ backends/evdev.md | 72 +++++++++ backends/loopback.md | 28 ++++ backends/midi.md | 54 +++++++ backends/ola.md | 41 +++++ backends/osc.md | 81 ++++++++++ backends/sacn.md | 58 +++++++ 8 files changed, 386 insertions(+), 418 deletions(-) create mode 100644 backends/artnet.md create mode 100644 backends/evdev.md create mode 100644 backends/loopback.md create mode 100644 backends/midi.md create mode 100644 backends/ola.md create mode 100644 backends/osc.md create mode 100644 backends/sacn.md diff --git a/README.md b/README.md index 8dfcdd8..6265581 100644 --- a/README.md +++ b/README.md @@ -30,42 +30,6 @@ on any other (or the same) supported protocol, for example to: * [Usage](#usage) * [Configuration](#configuration) * [Backend documentation](#backend-documentation) - + [The `artnet` backend](#the-artnet-backend) - - [Global configuration](#global-configuration) - - [Instance configuration](#instance-configuration) - - [Channel specification](#channel-specification) - - [Known bugs / problems](#known-bugs--problems) - + [The `sacn` backend](#the-sacn-backend) - - [Global configuration](#global-configuration-1) - - [Instance configuration](#instance-configuration-1) - - [Channel specification](#channel-specification-1) - - [Known bugs / problems](#known-bugs--problems-1) - + [The `midi` backend](#the-midi-backend) - - [Global configuration](#global-configuration-2) - - [Instance configuration](#instance-configuration-2) - - [Channel specification](#channel-specification-2) - - [Known bugs / problems](#known-bugs--problems-2) - + [The `evdev` backend](#the-evdev-backend) - - [Global configuration](#global-configuration-3) - - [Instance configuration](#instance-configuration-3) - - [Channel specification](#channel-specification-3) - - [Known bugs/problems](#known-bugs--problems-3) - + [The `loopback` backend](#the-loopback-backend) - - [Global configuration](#global-configuration-4) - - [Instance configuration](#instance-configuration-4) - - [Channel specification](#channel-specification-4) - - [Known bugs / problems](#known-bugs--problems-4) - + [The `osc` backend](#the-osc-backend) - - [Global configuration](#global-configuration-5) - - [Instance configuration](#instance-configuration-5) - - [Channel specification](#channel-specification-5) - - [Supported types & value ranges](#supported-types--value-ranges) - - [Known bugs / problems](#known-bugs--problems-5) - + [The `ola` backend](#the-ola-backend) - - [Global configuration](#global-configuration-6) - - [Instance configuration](#instance-configuration-6) - - [Channel specification](#channel-specification-6) - - [Known bugs / problems](#known-bugs--problems-6) * [Building](#building) + [Prerequisites](#prerequisites) + [Build](#build) @@ -112,389 +76,18 @@ The last line is a shorter way to create a bi-directional mapping. Example configuration files may be found in [configs/](configs/). ## Backend documentation -This section documents the configuration options supported by the various backends. -### The `artnet` backend - -The ArtNet backend provides read-write access to the UDP-based ArtNet protocol for lighting -fixture control. - -#### Global configuration - -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `bind` | `127.0.0.1 6454` | none | Binds a network address to listen for data. This option may be set multiple times, with each interface being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one interface is required for transmission. | -| `net` | `0` | `0` | The default net to use | - -#### Instance configuration - -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `net` | `0` | `0` | ArtNet `net` to use | -| `universe` | `0` | `0` | Universe identifier | -| `destination` | `10.2.2.2` | none | Destination address for sent ArtNet frames. Setting this enables the universe for output | -| `interface` | `1` | `0` | The bound address to use for data input/output | - -#### Channel specification - -A channel is specified by it's universe index. Channel indices start at 1 and end at 512. - -Example mapping: -``` -net1.231 < net2.123 -``` - -A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax -``` -net1.1+2 > net2.5+123 -``` - -A normal channel that is part of a wide channel can not be mapped individually. - -#### Known bugs / problems - -The minimum inter-frame-time is disregarded, as the packet rate is determined by the rate of incoming -channel events. - -### The `sacn` backend - -The sACN backend provides read-write access to the Multicast-UDP based streaming ACN protocol (ANSI E1.31-2016), -used for lighting fixture control. The backend sends universe discovery frames approximately every 10 seconds, -containing all write-enabled universes. - -#### Global configuration - -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `name` | `sACN source` | `MIDIMonster` | sACN source name | -| `cid` | `0xAA 0xBB 0xCC` ... | `MIDIMonster` | Source CID (16 bytes) | -| `bind` | `0.0.0.0 5568` | none | Binds a network address to listen for data. This option may be set multiple times, with each descriptor being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one descriptor is required for transmission. | - -#### Instance configuration - -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `universe` | `0` | none | Universe identifier | -| `interface` | `1` | `0` | The bound address to use for data input/output | -| `priority` | `100` | none | The data priority to transmit for this instance. Setting this option enables the instance for output and includes it in the universe discovery report. | -| `destination` | `10.2.2.2` | Universe multicast | Destination address for unicast output. If unset, the multicast destination for the specified universe is used. | -| `from` | `0xAA 0xBB` ... | none | 16-byte input source CID filter. Setting this option filters the input stream for this universe. | -| `unicast` | `1` | `0` | Prevent this instance from joining its universe multicast group | - -Note that instances accepting multicast input also process unicast frames directed at them, while -instances in `unicast` mode will not receive multicast frames. - -#### Channel specification - -A channel is specified by it's universe index. Channel indices start at 1 and end at 512. - -Example mapping: -``` -sacn1.231 < sacn2.123 -``` - -A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax -``` -sacn.1+2 > sacn2.5+123 -``` - -A normal channel that is part of a wide channel can not be mapped individually. - -#### Known bugs / problems - -The DMX start code of transmitted and received universes is fixed as `0`. - -The (upper) limit on packet transmission rate mandated by section 6.6.1 of the sACN specification is disregarded. -The rate of packet transmission is influenced by the rate of incoming mapped events on the instance. - -Universe synchronization is currently not supported, though this feature may be implemented in the future. - -To use multicast input, all networking hardware in the path must support the IGMPv2 protocol. - -The Linux kernel limits the number of multicast groups an interface may join to 20. An instance configured -for input automatically joins the multicast group for its universe, unless configured in `unicast` mode. -This limit can be raised by changing the kernel option in `/proc/sys/net/ipv4/igmp_max_memberships`. - -### The `midi` backend - -The MIDI backend provides read-write access to the MIDI protocol via virtual ports. - -#### Global configuration - -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `name` | `MIDIMonster` | none | MIDI client name | - -#### Instance configuration - -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `read` | `20:0` | none | MIDI device to connect for input | -| `write` | `DeviceName` | none | MIDI device to connect for output | - -MIDI device names may either be `client:port` portnames or prefixes of MIDI device names. -Run `aconnect -i` to list input ports and `aconnect -o` to list output ports. - -Each instance also provides a virtual port, so MIDI devices can also be connected with `aconnect `. - -#### Channel specification - -The MIDI backend supports multiple channel types - -* `cc` - Control Changes -* `note` - Note On/Off messages -* `nrpn` - NRPNs (not yet implemented) - -A channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead -of `channel`. -The earlier syntax of `.` is officially deprecated but still supported for compatability -reasons. This support may be removed at some future time. - -Channels range from `0` to `15`. Each channel consists of 128 notes (numbered `0` through `127`) and 128 CC's -(numbered likewise), a channel pressure control (also called 'channel aftertouch') and a pitch control. -Each Note also has an additional pressure value associated with it. - -Example mappings: -``` -midi1.ch0.note9 > midi2.channel1.cc4 -midi1.channel15.cc1 > midi1.channel0.note0 -``` -#### Known bugs / problems - -Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are -generated, which amount to the same thing according to the spec). This may be implemented as -a configuration option at a later time. - -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 `. - -### The `evdev` backend - -This backend allows using Linux `evdev` devices such as mouses, keyboards, gamepads and joysticks -as input and output devices. All buttons and axes available to the Linux system are mappable. -Output is provided by the `uinput` kernel module, which allows creation of virtual input devices. -This functionality may require elevated privileges (such as special group membership or root access). - -#### Global configuration - -This backend does not take any global configuration. - -#### Instance configuration - -| Option | Example value | Default value | Description | -|---------------|-----------------------|---------------|-------------------------------------------------------| -| `device` | `/dev/input/event1` | none | `evdev` device to use as input device | -| `input` | `Xbox Wireless` | none | Presentation name of evdev device to use as input (prefix-matched) | -| `output` | `My Input Device` | none | Output device presentation name. Setting this option enables the instance for output | -| `exclusive` | `1` | `0` | Prevent other processes from using the device | -| `id` | `0x1 0x2 0x3` | none | Set output device bus identification (Vendor, Product and Version), optional | -| `axis.AXISNAME`| `34300 0 65536 255 4095` | none | Specify absolute axis details (see below) for output. This is required for any absolute axis to be output. - -The absolute axis details configuration (e.g. `axis.ABS_X`) is required for any absolute axis on output-enabled -instances. The configuration value contains, space-separated, the following values: - -* `value`: The value to assume for the axis until an event is received -* `minimum`: The axis minimum value -* `maximum`: The axis maximum value -* `fuzz`: A value used for filtering the input stream -* `flat`: An offset, below which all deviations will be ignored -* `resolution`: Axis resolution in units per millimeter (or units per radian for rotational axes) - -For real devices, all of these parameters for every axis can be found by running `evtest` on the device. - -#### Channel specification - -A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes -see the [kernel documentation](https://www.kernel.org/doc/html/v4.12/input/event-codes.html). The most interesting event types are - -* `EV_KEY` for keys and buttons -* `EV_ABS` for absolute axes (such as Joysticks) -* `EV_REL` for relative axes (such as Mouses) - -The `evtest` tool is useful to gather information on devices active on the local system, including names, types, codes -and configuration supported by these devices. - -Example mapping: -``` -ev1.EV_KEY.KEY_A > ev1.EV_ABS.ABS_X -``` - -Note that to map an absolute axis on an output-enabled instance, additional information such as the axis minimum -and maximum are required. These must be specified in the instance configuration. When only mapping the instance -as a channel input, this is not required. - -#### Known bugs / problems - -Creating an `evdev` output device requires elevated privileges, namely, write access to the system's -`/dev/uinput`. Usually, this is granted for users in the `input` group and the `root` user. - -Input devices may synchronize logically connected event types (for example, X and Y axes) via `EV_SYN`-type -events. The MIDIMonster also generates these events after processing channel events, but may not keep the original -event grouping. - -Relative axes (`EV_REL`-type events), such as generated by mouses, are currently handled in a very basic fashion, -generating only the normalized channel values of `0`, `0.5` and `1` for any input less than, equal to and greater -than `0`, respectively. As for output, only the values `-1`, `0` and `1` are generated for the same interval. - -`EV_KEY` key-down events are sent for normalized channel values over `0.9`. - -Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file -but may or may not work with the internal channel mapping and normalization code. - -### The `loopback` backend - -This backend allows the user to create logical mapping channels, for example to exchange triggering -channels easier later. All events that are input are immediately output again on the same channel. - -#### Global configuration - -All global configuration is ignored. - -#### Instance configuration - -All instance configuration is ignored - -#### Channel specification - -A channel may have any string for a name. - -Example mapping: -``` -loop.foo < loop.bar123 -``` - -#### Known bugs / problems - -It is possible to configure loops using this backend. Triggering a loop -will create a deadlock, preventing any other backends from generating events. -Be careful with bidirectional channel mappings, as any input will be immediately -output to the same channel again. - -### The `osc` backend - -This backend offers read and write access to the Open Sound Control protocol, -spoken primarily by visual interface tools and hardware such as TouchOSC. - -#### Global configuration - -This backend does not take any global configuration. - -#### Instance configuration - -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `root` | `/my/osc/path` | none | An OSC path prefix to be prepended to all channels | -| `bind` | `:: 8000` | none | The host and port to listen on | -| `destination` | `10.11.12.13 8001` | none | Remote address to send OSC data to. Setting this enables the instance for output. The special value `learn` causes the MIDImonster to always reply to the address the last incoming packet came from. A different remote port for responses can be forced with the syntax `learn@` | - -Note that specifying an instance root speeds up matching, as packets not matching -it are ignored early in processing. - -Channels that are to be output or require a value range different from the default ranges (see below) -require special configuration, as their types and limits have to be set. - -This is done in the instance configuration using an assignment of the syntax - -``` -/local/osc/path = ... -``` - -The OSC path to be configured must only be the local part (omitting a configured instance root). - -**format** may be any sequence of valid OSC type characters. See below for a table of supported -OSC types. - -For each component of the path, the minimum and maximum values must be given separated by spaces. -Components may be accessed in the mapping section as detailed in the next section. - -An example configuration for transmission of an OSC message with 2 floating point components with -a range between 0.0 and 2.0 (for example, an X-Y control), would look as follows: - -``` -/1/xy1 = ff 0.0 2.0 0.0 2.0 -``` - -#### Channel specification - -A channel may be any valid OSC path, to which the instance root will be prepended if -set. Multi-value controls (such as X-Y pads) are supported by appending `:n` to the path, -where `n` is the parameter index, with the first (and default) one being `0`. - -Example mapping: -``` -osc1./1/xy1:0 > osc2./1/fader1 -``` - -Note that any channel that is to be output will need to be set up in the instance -configuration. - -#### Supported types & value ranges - -OSC allows controls to have individual value ranges and supports different parameter types. -The following types are currently supported by the MIDImonster: - -* **i**: 32-bit signed integer -* **f**: 32-bit IEEE floating point -* **h**: 64-bit signed integer -* **d**: 64-bit double precision floating point - -For each type, there is a default value range which will be assumed if the channel is not otherwise -configured using the instance configuration. Values out of a channels range will be clipped. - -The default ranges are: - -* **i**: `0` to `255` -* **f**: `0.0` to `1.0` -* **h**: `0` to `1024` -* **d**: `0.0` to `1.0` - -#### Known bugs / problems - -Ping requests are not yet answered. There may be some problems using broadcast output and input. - -### The `ola` backend - -This backend connects the MIDIMonster to the Open Lighting Architecture daemon. This can be useful -to take advantage of additional protocols implemented in OLA. This backend is currently marked as -optional and is only built with `make full` in the `backends/` directory, as the OLA is a large -dependency to require for all users. - -#### Global configuration - -This backend does not take any global configuration. - -#### Instance configuration - -| Option | Example value | Default value | Description | -|---------------|-----------------------|---------------|-------------------------------------------------------| -| `universe` | `7` | `0` | OLA universe to send/receive data on | - -#### Channel specification - -A channel is specified by it's universe index. Channel indices start at 1 and end at 512. - -Example mapping: -``` -ola1.231 < in2.123 -``` - -A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax -``` -ola1.1+2 > net2.5+123 -``` - -A normal channel that is part of a wide channel can not be mapped individually. - -#### Known bugs / problems - -The backend currently assumes that the OLA daemon is running on the same host as the MIDIMonster. -This may be made configurable in the future. - -This backend requires `libola-dev` to be installed, which pulls in a rather large and aggressive (in terms of probing -and taking over connected hardware) daemon. It is thus marked as optional and only built when executing the `full` target -within the `backends` directory. +Every backend includes specific documentation, including the global and instance +configuration options, channel specification syntax and any known problems or other +special information. These documentation files are located in the `backends/` directory. + +* [`midi` backend documentation](backends/midi.md) +* [`artnet` backend documentation](backends/artnet.md) +* [`sacn` backend documentation](backends/sacn.md) +* [`evdev` backend documentation](backends/evdev.md) +* [`loopback` backend documentation](backends/loopback.md) +* [`ola` backend documentation](backends/ola.md) +* [`osc` backend documentation](backends/osc.md) ## Building diff --git a/backends/artnet.md b/backends/artnet.md new file mode 100644 index 0000000..90a7697 --- /dev/null +++ b/backends/artnet.md @@ -0,0 +1,41 @@ +### The `artnet` backend + +The ArtNet backend provides read-write access to the UDP-based ArtNet protocol for lighting +fixture control. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `bind` | `127.0.0.1 6454` | none | Binds a network address to listen for data. This option may be set multiple times, with each interface being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one interface is required for transmission. | +| `net` | `0` | `0` | The default net to use | + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `net` | `0` | `0` | ArtNet `net` to use | +| `universe` | `0` | `0` | Universe identifier | +| `destination` | `10.2.2.2` | none | Destination address for sent ArtNet frames. Setting this enables the universe for output | +| `interface` | `1` | `0` | The bound address to use for data input/output | + +#### Channel specification + +A channel is specified by it's universe index. Channel indices start at 1 and end at 512. + +Example mapping: +``` +net1.231 < net2.123 +``` + +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax +``` +net1.1+2 > net2.5+123 +``` + +A normal channel that is part of a wide channel can not be mapped individually. + +#### Known bugs / problems + +The minimum inter-frame-time is disregarded, as the packet rate is determined by the rate of incoming +channel events. \ No newline at end of file diff --git a/backends/evdev.md b/backends/evdev.md new file mode 100644 index 0000000..dfe5ec9 --- /dev/null +++ b/backends/evdev.md @@ -0,0 +1,72 @@ +### The `evdev` backend + +This backend allows using Linux `evdev` devices such as mouses, keyboards, gamepads and joysticks +as input and output devices. All buttons and axes available to the Linux system are mappable. +Output is provided by the `uinput` kernel module, which allows creation of virtual input devices. +This functionality may require elevated privileges (such as special group membership or root access). + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|---------------|-------------------------------------------------------| +| `device` | `/dev/input/event1` | none | `evdev` device to use as input device | +| `input` | `Xbox Wireless` | none | Presentation name of evdev device to use as input (prefix-matched) | +| `output` | `My Input Device` | none | Output device presentation name. Setting this option enables the instance for output | +| `exclusive` | `1` | `0` | Prevent other processes from using the device | +| `id` | `0x1 0x2 0x3` | none | Set output device bus identification (Vendor, Product and Version), optional | +| `axis.AXISNAME`| `34300 0 65536 255 4095` | none | Specify absolute axis details (see below) for output. This is required for any absolute axis to be output. + +The absolute axis details configuration (e.g. `axis.ABS_X`) is required for any absolute axis on output-enabled +instances. The configuration value contains, space-separated, the following values: + +* `value`: The value to assume for the axis until an event is received +* `minimum`: The axis minimum value +* `maximum`: The axis maximum value +* `fuzz`: A value used for filtering the input stream +* `flat`: An offset, below which all deviations will be ignored +* `resolution`: Axis resolution in units per millimeter (or units per radian for rotational axes) + +For real devices, all of these parameters for every axis can be found by running `evtest` on the device. + +#### Channel specification + +A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes +see the [kernel documentation](https://www.kernel.org/doc/html/v4.12/input/event-codes.html). The most interesting event types are + +* `EV_KEY` for keys and buttons +* `EV_ABS` for absolute axes (such as Joysticks) +* `EV_REL` for relative axes (such as Mouses) + +The `evtest` tool is useful to gather information on devices active on the local system, including names, types, codes +and configuration supported by these devices. + +Example mapping: +``` +ev1.EV_KEY.KEY_A > ev1.EV_ABS.ABS_X +``` + +Note that to map an absolute axis on an output-enabled instance, additional information such as the axis minimum +and maximum are required. These must be specified in the instance configuration. When only mapping the instance +as a channel input, this is not required. + +#### Known bugs / problems + +Creating an `evdev` output device requires elevated privileges, namely, write access to the system's +`/dev/uinput`. Usually, this is granted for users in the `input` group and the `root` user. + +Input devices may synchronize logically connected event types (for example, X and Y axes) via `EV_SYN`-type +events. The MIDIMonster also generates these events after processing channel events, but may not keep the original +event grouping. + +Relative axes (`EV_REL`-type events), such as generated by mouses, are currently handled in a very basic fashion, +generating only the normalized channel values of `0`, `0.5` and `1` for any input less than, equal to and greater +than `0`, respectively. As for output, only the values `-1`, `0` and `1` are generated for the same interval. + +`EV_KEY` key-down events are sent for normalized channel values over `0.9`. + +Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file +but may or may not work with the internal channel mapping and normalization code. \ No newline at end of file diff --git a/backends/loopback.md b/backends/loopback.md new file mode 100644 index 0000000..a06c768 --- /dev/null +++ b/backends/loopback.md @@ -0,0 +1,28 @@ +### The `loopback` backend + +This backend allows the user to create logical mapping channels, for example to exchange triggering +channels easier later. All events that are input are immediately output again on the same channel. + +#### Global configuration + +All global configuration is ignored. + +#### Instance configuration + +All instance configuration is ignored + +#### Channel specification + +A channel may have any string for a name. + +Example mapping: +``` +loop.foo < loop.bar123 +``` + +#### Known bugs / problems + +It is possible (and very easy) to configure loops using this backend. Triggering a loop +will create a deadlock, preventing any other backends from generating events. +Be careful with bidirectional channel mappings, as any input will be immediately +output to the same channel again. \ No newline at end of file diff --git a/backends/midi.md b/backends/midi.md new file mode 100644 index 0000000..7d3e847 --- /dev/null +++ b/backends/midi.md @@ -0,0 +1,54 @@ +### The `midi` backend + +The MIDI backend provides read-write access to the MIDI protocol via virtual ports. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `name` | `MIDIMonster` | none | MIDI client name | + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `read` | `20:0` | none | MIDI device to connect for input | +| `write` | `DeviceName` | none | MIDI device to connect for output | + +MIDI device names may either be `client:port` portnames or prefixes of MIDI device names. +Run `aconnect -i` to list input ports and `aconnect -o` to list output ports. + +Each instance also provides a virtual port, so MIDI devices can also be connected with `aconnect `. + +#### Channel specification + +The MIDI backend supports multiple channel types + +* `cc` - Control Changes +* `note` - Note On/Off messages +* `nrpn` - NRPNs (not yet implemented) + +A channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead +of `channel`. +The earlier syntax of `.` is officially deprecated but still supported for compatability +reasons. This support may be removed at some future time. + +Channels range from `0` to `15`. Each channel consists of 128 notes (numbered `0` through `127`) and 128 CC's +(numbered likewise), a channel pressure control (also called 'channel aftertouch') and a pitch control. +Each Note also has an additional pressure value associated with it. + +Example mappings: +``` +midi1.ch0.note9 > midi2.channel1.cc4 +midi1.channel15.cc1 > midi1.channel0.note0 +``` +#### Known bugs / problems + +Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are +generated, which amount to the same thing according to the spec). This may be implemented as +a configuration option at a later time. + +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 `. \ No newline at end of file diff --git a/backends/ola.md b/backends/ola.md new file mode 100644 index 0000000..e3a1197 --- /dev/null +++ b/backends/ola.md @@ -0,0 +1,41 @@ +### The `ola` backend + +This backend connects the MIDIMonster to the Open Lighting Architecture daemon. This can be useful +to take advantage of additional protocols implemented in OLA. This backend is currently marked as +optional and is only built with `make full` in the `backends/` directory, as the OLA is a large +dependency to require for all users. + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|---------------|-------------------------------------------------------| +| `universe` | `7` | `0` | OLA universe to send/receive data on | + +#### Channel specification + +A channel is specified by it's universe index. Channel indices start at 1 and end at 512. + +Example mapping: +``` +ola1.231 < in2.123 +``` + +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax +``` +ola1.1+2 > net2.5+123 +``` + +A normal channel that is part of a wide channel can not be mapped individually. + +#### Known bugs / problems + +The backend currently assumes that the OLA daemon is running on the same host as the MIDIMonster. +This may be made configurable in the future. + +This backend requires `libola-dev` to be installed, which pulls in a rather large and aggressive (in terms of probing +and taking over connected hardware) daemon. It is thus marked as optional and only built when executing the `full` target +within the `backends` directory. \ No newline at end of file diff --git a/backends/osc.md b/backends/osc.md new file mode 100644 index 0000000..c784cda --- /dev/null +++ b/backends/osc.md @@ -0,0 +1,81 @@ +### The `osc` backend + +This backend offers read and write access to the Open Sound Control protocol, +spoken primarily by visual interface tools and hardware such as TouchOSC. + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `root` | `/my/osc/path` | none | An OSC path prefix to be prepended to all channels | +| `bind` | `:: 8000` | none | The host and port to listen on | +| `destination` | `10.11.12.13 8001` | none | Remote address to send OSC data to. Setting this enables the instance for output. The special value `learn` causes the MIDImonster to always reply to the address the last incoming packet came from. A different remote port for responses can be forced with the syntax `learn@` | + +Note that specifying an instance root speeds up matching, as packets not matching +it are ignored early in processing. + +Channels that are to be output or require a value range different from the default ranges (see below) +require special configuration, as their types and limits have to be set. + +This is done in the instance configuration using an assignment of the syntax + +``` +/local/osc/path = ... +``` + +The OSC path to be configured must only be the local part (omitting a configured instance root). + +**format** may be any sequence of valid OSC type characters. See below for a table of supported +OSC types. + +For each component of the path, the minimum and maximum values must be given separated by spaces. +Components may be accessed in the mapping section as detailed in the next section. + +An example configuration for transmission of an OSC message with 2 floating point components with +a range between 0.0 and 2.0 (for example, an X-Y control), would look as follows: + +``` +/1/xy1 = ff 0.0 2.0 0.0 2.0 +``` + +#### Channel specification + +A channel may be any valid OSC path, to which the instance root will be prepended if +set. Multi-value controls (such as X-Y pads) are supported by appending `:n` to the path, +where `n` is the parameter index, with the first (and default) one being `0`. + +Example mapping: +``` +osc1./1/xy1:0 > osc2./1/fader1 +``` + +Note that any channel that is to be output will need to be set up in the instance +configuration. + +#### Supported types & value ranges + +OSC allows controls to have individual value ranges and supports different parameter types. +The following types are currently supported by the MIDImonster: + +* **i**: 32-bit signed integer +* **f**: 32-bit IEEE floating point +* **h**: 64-bit signed integer +* **d**: 64-bit double precision floating point + +For each type, there is a default value range which will be assumed if the channel is not otherwise +configured using the instance configuration. Values out of a channels range will be clipped. + +The default ranges are: + +* **i**: `0` to `255` +* **f**: `0.0` to `1.0` +* **h**: `0` to `1024` +* **d**: `0.0` to `1.0` + +#### Known bugs / problems + +Ping requests are not yet answered. There may be some problems using broadcast output and input. \ No newline at end of file diff --git a/backends/sacn.md b/backends/sacn.md new file mode 100644 index 0000000..3d245a4 --- /dev/null +++ b/backends/sacn.md @@ -0,0 +1,58 @@ +### The `sacn` backend + +The sACN backend provides read-write access to the Multicast-UDP based streaming ACN protocol (ANSI E1.31-2016), +used for lighting fixture control. The backend sends universe discovery frames approximately every 10 seconds, +containing all write-enabled universes. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `name` | `sACN source` | `MIDIMonster` | sACN source name | +| `cid` | `0xAA 0xBB 0xCC` ... | `MIDIMonster` | Source CID (16 bytes) | +| `bind` | `0.0.0.0 5568` | none | Binds a network address to listen for data. This option may be set multiple times, with each descriptor being assigned an index starting from 0 to be used with the `interface` instance configuration option. At least one descriptor is required for transmission. | + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `universe` | `0` | none | Universe identifier | +| `interface` | `1` | `0` | The bound address to use for data input/output | +| `priority` | `100` | none | The data priority to transmit for this instance. Setting this option enables the instance for output and includes it in the universe discovery report. | +| `destination` | `10.2.2.2` | Universe multicast | Destination address for unicast output. If unset, the multicast destination for the specified universe is used. | +| `from` | `0xAA 0xBB` ... | none | 16-byte input source CID filter. Setting this option filters the input stream for this universe. | +| `unicast` | `1` | `0` | Prevent this instance from joining its universe multicast group | + +Note that instances accepting multicast input also process unicast frames directed at them, while +instances in `unicast` mode will not receive multicast frames. + +#### Channel specification + +A channel is specified by it's universe index. Channel indices start at 1 and end at 512. + +Example mapping: +``` +sacn1.231 < sacn2.123 +``` + +A 16-bit channel (spanning any two normal 8-bit channels in the same universe, also called a wide channel) may be mapped with the syntax +``` +sacn.1+2 > sacn2.5+123 +``` + +A normal channel that is part of a wide channel can not be mapped individually. + +#### Known bugs / problems + +The DMX start code of transmitted and received universes is fixed as `0`. + +The (upper) limit on packet transmission rate mandated by section 6.6.1 of the sACN specification is disregarded. +The rate of packet transmission is influenced by the rate of incoming mapped events on the instance. + +Universe synchronization is currently not supported, though this feature may be implemented in the future. + +To use multicast input, all networking hardware in the path must support the IGMPv2 protocol. + +The Linux kernel limits the number of multicast groups an interface may join to 20. An instance configured +for input automatically joins the multicast group for its universe, unless configured in `unicast` mode. +This limit can be raised by changing the kernel option in `/proc/sys/net/ipv4/igmp_max_memberships`. \ No newline at end of file -- cgit v1.2.3 From 6b97a08dee55ac4807902fc7ae1b4efe2911b873 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 1 Apr 2019 20:13:22 +0200 Subject: Implement pitch, aftertouch and pressure events for the MIDI backend --- backends/midi.c | 40 +++++++++++++++++++++++++++++++++++++++- backends/midi.md | 23 +++++++++++++++-------- monster.cfg | 15 ++++++++++++--- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index ad7f6fe..2999e6b 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -24,6 +24,9 @@ enum /*_midi_channel_type*/ { none = 0, note, cc, + pressure, + aftertouch, + pitchbend, nrpn, sysmsg }; @@ -174,6 +177,16 @@ static channel* midi_channel(instance* instance, char* spec){ ident.fields.type = nrpn; channel += 4; } + else if(!strncmp(channel, "pressure", 8)){ + ident.fields.type = pressure; + channel += 8; + } + else if(!strncmp(channel, "pitch", 8)){ + ident.fields.type = pitchbend; + } + else if(!strncmp(channel, "aftertouch", 10)){ + ident.fields.type = aftertouch; + } } ident.fields.control = strtoul(channel, NULL, 10); @@ -209,6 +222,16 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ case cc: snd_seq_ev_set_controller(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); break; + case pressure: + snd_seq_ev_set_keypress(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); + + break; + case pitchbend: + snd_seq_ev_set_pitchbend(&ev, ident.fields.channel, (v[u].normalised * 16383.0) - 8192); + break; + case aftertouch: + snd_seq_ev_set_chanpress(&ev, ident.fields.channel, v[u].normalised * 127.0); + break; case nrpn: //FIXME set to nrpn output break; @@ -239,13 +262,28 @@ static int midi_handle(size_t num, managed_fd* fds){ switch(ev->type){ case SND_SEQ_EVENT_NOTEON: case SND_SEQ_EVENT_NOTEOFF: - case SND_SEQ_EVENT_KEYPRESS: case SND_SEQ_EVENT_NOTE: ident.fields.type = note; ident.fields.channel = ev->data.note.channel; ident.fields.control = ev->data.note.note; val.normalised = (double)ev->data.note.velocity / 127.0; 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; + 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; + 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; + break; case SND_SEQ_EVENT_CONTROLLER: ident.fields.type = cc; ident.fields.channel = ev->data.control.channel; diff --git a/backends/midi.md b/backends/midi.md index 7d3e847..315edfe 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -22,25 +22,32 @@ Each instance also provides a virtual port, so MIDI devices can also be connecte #### Channel specification -The MIDI backend supports multiple channel types +The MIDI backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are * `cc` - Control Changes * `note` - Note On/Off messages +* `pressure` - Note pressure/aftertouch messages +* `aftertouch` - Channel-wide aftertouch messages +* `pitch` - Channel pitchbend messages * `nrpn` - NRPNs (not yet implemented) -A channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead -of `channel`. +A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be +used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). The earlier syntax of `.` is officially deprecated but still supported for compatability reasons. This support may be removed at some future time. -Channels range from `0` to `15`. Each channel consists of 128 notes (numbered `0` through `127`) and 128 CC's -(numbered likewise), a channel pressure control (also called 'channel aftertouch') and a pitch control. -Each Note also has an additional pressure value associated with it. +The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. + +MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which +additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called +'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. Example mappings: ``` midi1.ch0.note9 > midi2.channel1.cc4 -midi1.channel15.cc1 > midi1.channel0.note0 +midi1.channel15.pressure1 > midi1.channel0.note0 +midi1.aftertouch > midi2.cc0 +midi1.pitch > midi2.pitch ``` #### Known bugs / problems @@ -51,4 +58,4 @@ 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 `. \ No newline at end of file +list all incoming events using `aseqdump -p `. diff --git a/monster.cfg b/monster.cfg index d7c31dc..0760571 100644 --- a/monster.cfg +++ b/monster.cfg @@ -11,6 +11,9 @@ bind = 0.0.0.0 universe = 1 dest = 129.13.215.0 +[backend midi] +name = Monster + ;[evdev in] ;input = Xbox Wireless Controller @@ -19,8 +22,9 @@ universe = 1 priority = 100 [midi midi] +read = Axiom -[ola ola] +;[ola ola] [map] ;in.EV_ABS.ABS_X > sacn.1+2 @@ -33,5 +37,10 @@ priority = 100 ;in.EV_KEY.BTN_THUMBR > sacn.5 ;in.EV_ABS.ABS_GAS > sacn.6+7 ;in.EV_ABS.ABS_BRAKE > sacn.8 -ola.1 > midi.cc0.1 -ola.2+3 > midi.cc0.2 +;ola.1 > midi.cc0.1 +;ola.2+3 > midi.cc0.2 +midi.ch0.pitch > midi.ch0.cc1 +midi.ch0.aftertouch > midi.ch0.cc2 +midi.ch0.cc71 > midi.ch0.pitch +midi.ch0.cc74 > midi.ch0.aftertouch +midi.ch0.cc91 > midi.ch0.pressure1 -- cgit v1.2.3 From dd3ecfd3312673c8cfcfd8fbe96586b23d873c07 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 1 Apr 2019 20:15:50 +0200 Subject: Fix spelling --- backends/midi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/midi.md b/backends/midi.md index 315edfe..da9870f 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -33,7 +33,7 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). -The earlier syntax of `.` is officially deprecated but still supported for compatability +The earlier syntax of `.` is officially deprecated but still supported for compatibility reasons. This support may be removed at some future time. The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. -- cgit v1.2.3 From f4ca1869c3e1a106e4dbab39213ba81141ada1d5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 1 Apr 2019 20:20:12 +0200 Subject: Fix MIDI example mappings --- backends/midi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/midi.md b/backends/midi.md index da9870f..9733cc2 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -46,8 +46,8 @@ Example mappings: ``` midi1.ch0.note9 > midi2.channel1.cc4 midi1.channel15.pressure1 > midi1.channel0.note0 -midi1.aftertouch > midi2.cc0 -midi1.pitch > midi2.pitch +midi1.ch1.aftertouch > midi2.ch2.cc0 +midi1.ch0.pitch > midi2.ch1.pitch ``` #### Known bugs / problems -- cgit v1.2.3 From 68f1a58f13997cf052241d6e1177dcbec9a109ec Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 29 Jun 2019 11:54:23 +0200 Subject: Update loopback backend to match API design --- backends/evdev.c | 1 + backends/loopback.c | 42 ++++++++++++++++++++++-------------------- backends/loopback.h | 18 +++++++++--------- backends/osc.c | 1 + 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/backends/evdev.c b/backends/evdev.c index 979698f..f528d06 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -468,5 +468,6 @@ static int evdev_shutdown(){ } free(instances); + fprintf(stderr, "evdev backend shut down\n"); return 0; } diff --git a/backends/loopback.c b/backends/loopback.c index bb93a1f..083a312 100644 --- a/backends/loopback.c +++ b/backends/loopback.c @@ -6,14 +6,14 @@ int init(){ backend loopback = { .name = BACKEND_NAME, - .conf = backend_configure, - .create = backend_instance, - .conf_instance = backend_configure_instance, - .channel = backend_channel, - .handle = backend_set, - .process = backend_handle, - .start = backend_start, - .shutdown = backend_shutdown + .conf = loopback_configure, + .create = loopback_instance, + .conf_instance = loopback_configure_instance, + .channel = loopback_channel, + .handle = loopback_set, + .process = loopback_handle, + .start = loopback_start, + .shutdown = loopback_shutdown }; //register backend @@ -24,23 +24,23 @@ int init(){ return 0; } -static int backend_configure(char* option, char* value){ +static int loopback_configure(char* option, char* value){ //intentionally ignored return 0; } -static int backend_configure_instance(instance* inst, char* option, char* value){ +static int loopback_configure_instance(instance* inst, char* option, char* value){ //intentionally ignored return 0; } -static instance* backend_instance(){ +static instance* loopback_instance(){ instance* i = mm_instance(); if(!i){ return NULL; } - i->impl = calloc(1, sizeof(loopback_instance)); + i->impl = calloc(1, sizeof(loopback_instance_data)); if(!i->impl){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; @@ -49,9 +49,9 @@ static instance* backend_instance(){ return i; } -static channel* backend_channel(instance* inst, char* spec){ +static channel* loopback_channel(instance* inst, char* spec){ size_t u; - loopback_instance* data = (loopback_instance*) inst->impl; + loopback_instance_data* data = (loopback_instance_data*) inst->impl; //find matching channel for(u = 0; u < data->n; u++){ @@ -79,7 +79,7 @@ static channel* backend_channel(instance* inst, char* spec){ return mm_channel(inst, u, 1); } -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v){ +static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t n; for(n = 0; n < num; n++){ mm_channel_event(c[n], v[n]); @@ -87,19 +87,19 @@ static int backend_set(instance* inst, size_t num, channel** c, channel_value* v return 0; } -static int backend_handle(size_t num, managed_fd* fds){ +static int loopback_handle(size_t num, managed_fd* fds){ //no events generated here return 0; } -static int backend_start(){ +static int loopback_start(){ return 0; } -static int backend_shutdown(){ +static int loopback_shutdown(){ size_t n, u, p; instance** inst = NULL; - loopback_instance* data = NULL; + loopback_instance_data* data = NULL; if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); @@ -107,7 +107,7 @@ static int backend_shutdown(){ } for(u = 0; u < n; u++){ - data = (loopback_instance*) inst[u]->impl; + data = (loopback_instance_data*) inst[u]->impl; for(p = 0; p < data->n; p++){ free(data->name[p]); } @@ -116,5 +116,7 @@ static int backend_shutdown(){ } free(inst); + + fprintf(stderr, "Loopback backend shut down\n"); return 0; } diff --git a/backends/loopback.h b/backends/loopback.h index fe44e91..c73ca20 100644 --- a/backends/loopback.h +++ b/backends/loopback.h @@ -1,16 +1,16 @@ #include "midimonster.h" int init(); -static int backend_configure(char* option, char* value); -static int backend_configure_instance(instance* instance, char* option, char* value); -static instance* backend_instance(); -static channel* backend_channel(instance* instance, char* spec); -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v); -static int backend_handle(size_t num, managed_fd* fds); -static int backend_start(); -static int backend_shutdown(); +static int loopback_configure(char* option, char* value); +static int loopback_configure_instance(instance* inst, char* option, char* value); +static instance* loopback_instance(); +static channel* loopback_channel(instance* inst, char* spec); +static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v); +static int loopback_handle(size_t num, managed_fd* fds); +static int loopback_start(); +static int loopback_shutdown(); typedef struct /*_loopback_instance_data*/ { size_t n; char** name; -} loopback_instance; +} loopback_instance_data; diff --git a/backends/osc.c b/backends/osc.c index 9996f68..130dd2d 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -705,5 +705,6 @@ static int backend_shutdown(){ } free(inst); + fprintf(stderr, "OSC backend shut down\n"); return 0; } -- cgit v1.2.3 From ee75bee08b8fb280fc1d76e8635cf29c576835da Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 30 Jun 2019 14:13:22 +0200 Subject: Update OSC backend to API design --- backends/osc.c | 52 ++++++++++++++++++++++++++-------------------------- backends/osc.h | 18 +++++++++--------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/backends/osc.c b/backends/osc.c index 130dd2d..fd1bcd4 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -16,14 +16,14 @@ int init(){ backend osc = { .name = BACKEND_NAME, - .conf = backend_configure, - .create = backend_instance, - .conf_instance = backend_configure_instance, - .channel = backend_channel, - .handle = backend_set, - .process = backend_handle, - .start = backend_start, - .shutdown = backend_shutdown + .conf = osc_configure, + .create = osc_instance, + .conf_instance = osc_configure_instance, + .channel = osc_map_channel, + .handle = osc_set, + .process = osc_handle, + .start = osc_start, + .shutdown = osc_shutdown }; //register backend @@ -253,13 +253,13 @@ static int osc_validate_path(char* path){ return 0; } -static int backend_configure(char* option, char* value){ +static int osc_configure(char* option, char* value){ fprintf(stderr, "The OSC backend does not take any global configuration\n"); return 1; } -static int backend_configure_instance(instance* inst, char* option, char* value){ - osc_instance* data = (osc_instance*) inst->impl; +static int osc_configure_instance(instance* inst, char* option, char* value){ + osc_instance_data* data = (osc_instance_data*) inst->impl; char* host = NULL, *port = NULL, *token = NULL, *format = NULL; size_t u, p; @@ -394,13 +394,13 @@ static int backend_configure_instance(instance* inst, char* option, char* value) return 1; } -static instance* backend_instance(){ +static instance* osc_instance(){ instance* inst = mm_instance(); if(!inst){ return NULL; } - osc_instance* data = calloc(1, sizeof(osc_instance)); + osc_instance_data* data = calloc(1, sizeof(osc_instance_data)); if(!data){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; @@ -411,9 +411,9 @@ static instance* backend_instance(){ return inst; } -static channel* backend_channel(instance* inst, char* spec){ +static channel* osc_map_channel(instance* inst, char* spec){ size_t u; - osc_instance* data = (osc_instance*) inst->impl; + osc_instance_data* data = (osc_instance_data*) inst->impl; size_t param_index = 0; //check spec for correctness @@ -457,14 +457,14 @@ static channel* backend_channel(instance* inst, char* spec){ return mm_channel(inst, u, 1); } -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v){ +static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ uint8_t xmit_buf[OSC_XMIT_BUF], *format = NULL; size_t evt = 0, off, members, p; if(!num){ return 0; } - osc_instance* data = (osc_instance*) inst->impl; + osc_instance_data* data = (osc_instance_data*) inst->impl; if(!data->dest_len){ fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%zu channels)\n", inst->name, num); return 0; @@ -563,11 +563,11 @@ static int backend_set(instance* inst, size_t num, channel** c, channel_value* v return 0; } -static int backend_handle(size_t num, managed_fd* fds){ +static int osc_handle(size_t num, managed_fd* fds){ size_t fd; char recv_buf[OSC_RECV_BUF]; instance* inst = NULL; - osc_instance* data = NULL; + osc_instance_data* data = NULL; ssize_t bytes_read = 0; size_t c; char* osc_fmt = NULL; @@ -581,7 +581,7 @@ static int backend_handle(size_t num, managed_fd* fds){ continue; } - data = (osc_instance*) inst->impl; + data = (osc_instance_data*) inst->impl; do{ if(data->learn){ @@ -639,10 +639,10 @@ static int backend_handle(size_t num, managed_fd* fds){ return 0; } -static int backend_start(){ +static int osc_start(){ size_t n, u, fds = 0; instance** inst = NULL; - osc_instance* data = NULL; + osc_instance_data* data = NULL; //fetch all instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ @@ -657,7 +657,7 @@ static int backend_start(){ //update instance identifiers for(u = 0; u < n; u++){ - data = (osc_instance*) inst[u]->impl; + data = (osc_instance_data*) inst[u]->impl; if(data->fd >= 0){ inst[u]->ident = data->fd; @@ -679,10 +679,10 @@ static int backend_start(){ return 0; } -static int backend_shutdown(){ +static int osc_shutdown(){ size_t n, u, c; instance** inst = NULL; - osc_instance* data = NULL; + osc_instance_data* data = NULL; if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); @@ -690,7 +690,7 @@ static int backend_shutdown(){ } for(u = 0; u < n; u++){ - data = (osc_instance*) inst[u]->impl; + data = (osc_instance_data*) inst[u]->impl; for(c = 0; c < data->channels; c++){ free(data->channel[c].path); } diff --git a/backends/osc.h b/backends/osc.h index 5938f12..dc6cb3a 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -6,14 +6,14 @@ #define OSC_XMIT_BUF 8192 int init(); -static int backend_configure(char* option, char* value); -static int backend_configure_instance(instance* instance, char* option, char* value); -static instance* backend_instance(); -static channel* backend_channel(instance* instance, char* spec); -static int backend_set(instance* inst, size_t num, channel** c, channel_value* v); -static int backend_handle(size_t num, managed_fd* fds); -static int backend_start(); -static int backend_shutdown(); +static int osc_configure(char* option, char* value); +static int osc_configure_instance(instance* inst, char* option, char* value); +static instance* osc_instance(); +static channel* osc_map_channel(instance* inst, char* spec); +static int osc_set(instance* inst, size_t num, channel** c, channel_value* v); +static int osc_handle(size_t num, managed_fd* fds); +static int osc_start(); +static int osc_shutdown(); typedef enum { not_set = 0, @@ -52,4 +52,4 @@ typedef struct /*_osc_instance_data*/ { int fd; uint8_t learn; uint16_t forced_rport; -} osc_instance; +} osc_instance_data; -- cgit v1.2.3 From b618c4a6b74a52f830ca53029e1cc680d56a2501 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 6 Jul 2019 17:25:12 +0200 Subject: Implement Lua backend --- .travis.yml | 1 + README.md | 13 ++-- backends/Makefile | 4 +- backends/lua.c | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/lua.h | 21 ++++++ backends/lua.md | 49 +++++++++++++ configs/demo.lua | 21 ++++++ configs/lua.cfg | 35 ++++++++++ midimonster.h | 1 + 9 files changed, 342 insertions(+), 5 deletions(-) create mode 100644 backends/lua.c create mode 100644 backends/lua.h create mode 100644 backends/lua.md create mode 100644 configs/demo.lua create mode 100644 configs/lua.cfg diff --git a/.travis.yml b/.travis.yml index ab047ce..66bfd9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ addons: - libasound2-dev - libevdev-dev - libola-dev + - liblua5.3-dev packages: &core_build_gpp_latest - *core_build - gcc-8 diff --git a/README.md b/README.md index 6265581..ee907a6 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,15 @@ Currently, the MIDIMonster supports the following protocols: * evdev input devices (Linux) * Open Lighting Architecture (OLA) +with additional flexibility provided by a Lua scripting environment. + The MIDIMonster allows the user to translate any channel on one protocol into channel(s) on any other (or the same) supported protocol, for example to: * Translate MIDI Control Changes into Notes ([Example configuration](configs/unifest-17.cfg)) * Translate MIDI Notes into ArtNet or sACN ([Example configuration](configs/launchctl-sacn.cfg)) * Translate OSC messages into MIDI ([Example configuration](configs/midi-osc.cfg)) +* Dynamically route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller * 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 ([Example configuration](configs/evdev.conf)) @@ -88,6 +91,7 @@ special information. These documentation files are located in the `backends/` di * [`loopback` backend documentation](backends/loopback.md) * [`ola` backend documentation](backends/ola.md) * [`osc` backend documentation](backends/osc.md) +* [`lua` backend documentation](backends/lua.md) ## Building @@ -99,10 +103,11 @@ This section will explain how to build the provided sources to be able to run In order to build the MIDIMonster, you'll need some libraries that provide support for the protocols to translate. -* libasound2-dev (for the MIDI backend) -* libevdev-dev (for the evdev backend) -* libola-dev (for the optional OLA backend) -* pkg-config (as some projects and systems like to spread their files around) +* `libasound2-dev` (for the MIDI backend) +* `libevdev-dev` (for the evdev backend) +* `liblua5.3-dev` (for the lua backend) +* `libola-dev` (for the optional OLA backend) +* `pkg-config` (as some projects and systems like to spread their files around) * A C compiler * GNUmake diff --git a/backends/Makefile b/backends/Makefile index c11de56..fe88669 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full OPTIONAL_BACKENDS = ola.so LINUX_BACKENDS = midi.so evdev.so -BACKENDS = artnet.so osc.so loopback.so sacn.so +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) @@ -27,6 +27,8 @@ evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) ola.so: LDLIBS = -lola ola.so: CPPFLAGS += -Wno-write-strings +lua.so: CFLAGS += $(shell pkg-config --cflags lua5.3) +lua.so: LDLIBS += $(shell pkg-config --libs lua5.3) %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) diff --git a/backends/lua.c b/backends/lua.c new file mode 100644 index 0000000..ae2d460 --- /dev/null +++ b/backends/lua.c @@ -0,0 +1,202 @@ +#include +#include "lua.h" + +#define BACKEND_NAME "lua" +#define LUA_REGISTRY_KEY "_midimonster_lua_instance" + +//TODO instance identification for callvacks + +int init(){ + backend lua = { + .name = BACKEND_NAME, + .conf = lua_configure, + .create = lua_instance, + .conf_instance = lua_configure_instance, + .channel = lua_channel, + .handle = lua_set, + .process = lua_handle, + .start = lua_start, + .shutdown = lua_shutdown + }; + + //register backend + if(mm_backend_register(lua)){ + fprintf(stderr, "Failed to register lua backend\n"); + return 1; + } + return 0; +} + +static int lua_callback_output(lua_State* interpreter){ + int arguments = lua_gettop(interpreter); + size_t n; + channel_value val; + const char* channel_name = NULL; + channel* channel = NULL; + instance* inst = NULL; + lua_instance_data* data = NULL; + + if(arguments != 2){ + fprintf(stderr, "Lua output function called with %d arguments, expected 2\n", arguments); + return 0; + } + + channel_name = lua_tostring(interpreter, 1); + val.normalised = clamp(lua_tonumber(interpreter, 2), 1.0, 0.0); + + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance *) lua_touserdata(interpreter, -1); + data = (lua_instance_data*) inst->impl; + + for(n = 0; n < data->channels; n++){ + if(!strcmp(channel_name, data->channel_name[n])){ + channel = mm_channel(inst, n, 0); + if(!channel){ + return 0; + } + mm_channel_event(channel, val); + return 0; + } + } + + fprintf(stderr, "Tried to set unknown channel %s.%s\n", inst->name, channel_name); + return 0; +} + +static int lua_configure(char* option, char* value){ + fprintf(stderr, "The lua backend does not take any global configuration\n"); + return 1; +} + +static int lua_configure_instance(instance* inst, char* option, char* value){ + lua_instance_data* data = (lua_instance_data*) inst->impl; + + if(!strcmp(option, "script")){ + if(luaL_dofile(data->interpreter, value)){ + fprintf(stderr, "Failed to load lua source file %s for instance %s: %s\n", value, inst->name, lua_tostring(data->interpreter, -1)); + return 1; + } + return 0; + } + + fprintf(stderr, "Unknown configuration parameter %s for lua backend\n", option); + return 1; +} + +static instance* lua_instance(){ + instance* inst = mm_instance(); + if(!inst){ + return NULL; + } + + lua_instance_data* data = calloc(1, sizeof(lua_instance_data)); + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + //load the interpreter + data->interpreter = luaL_newstate(); + if(!data->interpreter){ + fprintf(stderr, "Failed to initialize LUA\n"); + free(data); + return NULL; + } + luaL_openlibs(data->interpreter); + + //register lua api functions + lua_register(data->interpreter, "output", lua_callback_output); + + //store instance pointer to the lua state + lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); + lua_pushlightuserdata(data->interpreter, (void *) inst); + lua_settable(data->interpreter, LUA_REGISTRYINDEX); + + inst->impl = data; + return inst; +} + +static channel* lua_channel(instance* inst, char* spec){ + size_t u; + lua_instance_data* data = (lua_instance_data*) inst->impl; + + //find matching channel + for(u = 0; u < data->channels; u++){ + if(!strcmp(spec, data->channel_name[u])){ + break; + } + } + + //allocate new channel + if(u == data->channels){ + data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*)); + if(!data->channel_name){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + data->channel_name[u] = strdup(spec); + if(!data->channel_name[u]){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + data->channels++; + } + + return mm_channel(inst, u, 1); +} + +static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ + size_t n = 0; + lua_instance_data* data = (lua_instance_data*) inst->impl; + + for(n = 0; n < num; n++){ + //call lua channel handlers + lua_getglobal(data->interpreter, data->channel_name[c[n]->ident]); + lua_pushnumber(data->interpreter, v[n].normalised); + if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ + fprintf(stderr, "Failed to call handler for %s.%s: %s\n", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); + lua_pop(data->interpreter, 1); + } + } + return 0; +} + +static int lua_handle(size_t num, managed_fd* fds){ + //TODO call timer callbacks + return 0; +} + +static int lua_start(){ + //TODO start timers / register fds + return 0; +} + +static int lua_shutdown(){ + size_t n, u, p; + instance** inst = NULL; + lua_instance_data* data = NULL; + + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(u = 0; u < n; u++){ + data = (lua_instance_data*) inst[u]->impl; + //stop the interpreter + lua_close(data->interpreter); + //cleanup channel data + for(p = 0; p < data->channels; p++){ + free(data->channel_name[p]); + } + free(data->channel_name); + free(inst[u]->impl); + } + + free(inst); + + fprintf(stderr, "Lua backend shut down\n"); + return 0; +} diff --git a/backends/lua.h b/backends/lua.h new file mode 100644 index 0000000..27e1afd --- /dev/null +++ b/backends/lua.h @@ -0,0 +1,21 @@ +#include "midimonster.h" + +#include +#include +#include + +int init(); +static int lua_configure(char* option, char* value); +static int lua_configure_instance(instance* inst, char* option, char* value); +static instance* lua_instance(); +static channel* lua_channel(instance* inst, char* spec); +static int lua_set(instance* inst, size_t num, channel** c, channel_value* v); +static int lua_handle(size_t num, managed_fd* fds); +static int lua_start(); +static int lua_shutdown(); + +typedef struct /*_lua_instance_data*/ { + size_t channels; + char** channel_name; + lua_State* interpreter; +} lua_instance_data; diff --git a/backends/lua.md b/backends/lua.md new file mode 100644 index 0000000..91e8fe2 --- /dev/null +++ b/backends/lua.md @@ -0,0 +1,49 @@ +### The `lua` backend + +The `lua` backend provides a flexible programming environment, allowing users to route and manipulate +events using the Lua programming language. + +Every instance has it's own interpreter state which can be loaded with custom handler scripts. + +To process incoming channel events, the MIDIMonster calls corresponding Lua functions with +the value (as a Lua `number` type) as parameter. To send output on a channel, the Lua environment +provides the function `output(channel-name, value)`. + +Example script: +``` +function bar(value) + output("foo", value / 2) +end +``` + +Input values range between 0.0 and 1.0, output values are clamped to the same range. + +#### Global configuration + +The backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `source` | `script.lua` | none | Lua source file | + +A single instance may have multiple `source` options specified, which will all be read cumulatively. + +#### Channel specification + +Channel names may be any valid Lua function name. + +Example mapping: +``` +lua1.foo > lua2.bar +``` + +#### Known bugs / problems + +Using `output` as an input channel name to a Lua instance does not work, as the interpreter has +`output` globally assigned to the event output function. Using `output` as an output channel name +via `output("output", value)` works as intended. + +The path to the Lua source files is relative to the current working directory. This may lead +to problems when copying configuration between installations. diff --git a/configs/demo.lua b/configs/demo.lua new file mode 100644 index 0000000..e816ac4 --- /dev/null +++ b/configs/demo.lua @@ -0,0 +1,21 @@ +-- This example MIDIMonstaer Lua script spreads one input channel onto multiple output +-- channels using a polynomial function evaluated at multiple points. This effect can +-- be visualized e.g. with martrix (https://github.com/cbdevnet/martrix). + +-- This is just a demonstration of global variables +foo = 0 + +-- The polynomial to evaluate +function polynomial(offset, x) + return math.exp(-20 * (x - offset) ^ 2) +end + +-- Handler function for the input channel +function input(value) + foo = foo + 1 + print("input at ", value, foo) + + for chan=0,10 do + output("out" .. chan, polynomial(value, (1 / 10) * chan)) + end +end diff --git a/configs/lua.cfg b/configs/lua.cfg new file mode 100644 index 0000000..9182122 --- /dev/null +++ b/configs/lua.cfg @@ -0,0 +1,35 @@ +; This configuration uses a Lua script to distribute one input channel (from either a mouse +; button or an axis control) onto multiple output channels (on ArtNet). + +[backend artnet] +bind = 0.0.0.0 + +[evdev mouse] +device = /dev/input/by-path/platform-i8042-serio-2-event-mouse + +[evdev xbox] +input = Xbox Wireless +axis.ABS_X = 34300 0 65535 255 4095 + +[lua lua] +script = configs/demo.lua + +[artnet art] +universe = 0 +destination = 255.255.255.255 + +[map] +mouse.EV_KEY.BTN_LEFT > lua.input +xbox.EV_ABS.ABS_X > lua.input + +art.1 < lua.out0 +art.2 < lua.out1 +art.3 < lua.out2 +art.4 < lua.out3 +art.5 < lua.out4 +art.6 < lua.out5 +art.7 < lua.out6 +art.8 < lua.out7 +art.9 < lua.out8 +art.10 < lua.out9 +art.11 < lua.out10 diff --git a/midimonster.h b/midimonster.h index 572b5fb..8a18155 100644 --- a/midimonster.h +++ b/midimonster.h @@ -5,6 +5,7 @@ #include #define max(a,b) (((a) > (b)) ? (a) : (b)) #define min(a,b) (((a) < (b)) ? (a) : (b)) +#define clamp(val,max,min) (((val) > (max)) ? (max) : (((val) < (min)) ? (min) : (val))) #ifdef DEBUG #define DBGPF(format, ...) fprintf(stderr, (format), __VA_ARGS__) #define DBG(message) fprintf(stderr, "%s", (message)) -- cgit v1.2.3 From 62b41d0b2ea58b48961308ab24fe4287365b2d50 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 6 Jul 2019 17:44:10 +0200 Subject: evdev backend code style fixes --- TODO | 5 ++++- backends/evdev.c | 12 +++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/TODO b/TODO index dd3022b..cfdd409 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,8 @@ MIDI NRPN Note source in channel value struct Optimize core channel search (store backend offset) -Function generator Printing backend + +document example configs +lua timer +evdev relaxes size diff --git a/backends/evdev.c b/backends/evdev.c index f528d06..8871ce4 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -176,23 +176,27 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { return 1; } free(next_token); + return 0; } else if(!strcmp(option, "exclusive")){ if(data->input_fd >= 0 && libevdev_grab(data->input_ev, LIBEVDEV_GRAB)){ fprintf(stderr, "Failed to obtain exclusive device access on %s\n", inst->name); } data->exclusive = 1; + return 0; } #ifndef EVDEV_NO_UINPUT else if(!strcmp(option, "output")){ data->output_enabled = 1; libevdev_set_name(data->output_proto, value); + return 0; } else if(!strcmp(option, "id")){ next_token = value; libevdev_set_id_vendor(data->output_proto, strtol(next_token, &next_token, 0)); libevdev_set_id_product(data->output_proto, strtol(next_token, &next_token, 0)); libevdev_set_id_version(data->output_proto, strtol(next_token, &next_token, 0)); + return 0; } else if(!strncmp(option, "axis.", 5)){ //value minimum maximum fuzz flat resolution @@ -207,13 +211,11 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { fprintf(stderr, "Failed to enable absolute axis %s for output\n", option + 5); return 1; } + return 0; } #endif - else{ - fprintf(stderr, "Unknown configuration parameter %s for evdev backend\n", option); - return 1; - } - return 0; + fprintf(stderr, "Unknown configuration parameter %s for evdev backend\n", option); + return 1; } static channel* evdev_channel(instance* inst, char* spec){ -- cgit v1.2.3 From 4681120c21567df21d09903cdb2d3d8387786421 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 7 Jul 2019 10:15:32 +0200 Subject: Add lua dependency to macos for travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 66bfd9a..74af4c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -159,7 +159,7 @@ install: before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then PATH=/usr/local/opt/ccache/libexec:$PATH; fi # Use ccache on Mac too #Coverity doesn't work with g++ 5 or 6, so only upgrade to g++ 4.9 for that - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) -a "$CC" = "gcc" ]; then export CC="ccache gcc-8"; export CXX="ccache g++-8"; fi -- cgit v1.2.3 From 86b9706220ca285db961ea43ec0859ea99cc9f71 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 7 Jul 2019 12:30:10 +0200 Subject: Minor fixes --- backends/evdev.c | 4 ++++ backends/osc.c | 10 ++-------- midimonster.c | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/backends/evdev.c b/backends/evdev.c index 8871ce4..ca8469a 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -378,6 +378,10 @@ static int evdev_start(){ fds++; } + if(data->input_fd <= 0 && !data->output_ev){ + fprintf(stderr, "Instance %s has neither input nor output device set up\n", inst[u]->name); + } + } fprintf(stderr, "evdev backend registered %zu descriptors to core\n", fds); diff --git a/backends/osc.c b/backends/osc.c index fd1bcd4..1305169 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -167,14 +167,8 @@ static inline channel_value osc_parameter_normalise(osc_parameter_type t, osc_pa fprintf(stderr, "Invalid OSC type passed to interpolation routine\n"); } - //fix overshoot - if(v.normalised > 1.0){ - v.normalised = 1.0; - } - else if(v.normalised < 0.0){ - v.normalised = 0.0; - } - + //clamp to range + v.normalised = clamp(v.normalised, 1.0, 0.0); return v; } diff --git a/midimonster.c b/midimonster.c index 4784052..fb664a4 100644 --- a/midimonster.c +++ b/midimonster.c @@ -324,7 +324,7 @@ int main(int argc, char** argv){ while(primary->n){ //swap primary and secondary event collectors DBGPF("Swapping event collectors, %zu events in primary\n", primary->n); - for(u = 0; u < sizeof(event_pool)/sizeof(event_collection); u++){ + for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ if(primary != event_pool + u){ secondary = primary; primary = event_pool + u; -- cgit v1.2.3 From ee4a46105acecb6a7adc1e7189e8b0a66404b421 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 13 Jul 2019 17:51:11 +0200 Subject: Improved Lua backend with intervals --- TODO | 2 +- backends/lua.c | 302 +++++++++++++++++++++++++++++++++++++++++++++++++++---- backends/lua.h | 10 ++ backends/lua.md | 32 ++++-- configs/demo.lua | 45 ++++++--- configs/lua.cfg | 10 +- 6 files changed, 359 insertions(+), 42 deletions(-) diff --git a/TODO b/TODO index cfdd409..f39dae0 100644 --- a/TODO +++ b/TODO @@ -5,4 +5,4 @@ Printing backend document example configs lua timer -evdev relaxes size +evdev relative axis size diff --git a/backends/lua.c b/backends/lua.c index ae2d460..08d8d3c 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -1,10 +1,18 @@ #include +#include +#include +#include #include "lua.h" #define BACKEND_NAME "lua" #define LUA_REGISTRY_KEY "_midimonster_lua_instance" -//TODO instance identification for callvacks +static size_t timers = 0; +static lua_timer* timer = NULL; +static struct itimerspec timer_config = { + 0 +}; +static int timer_fd = -1; int init(){ backend lua = { @@ -24,31 +32,83 @@ int init(){ fprintf(stderr, "Failed to register lua backend\n"); return 1; } + + //create the timer to expire intervals + timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); + if(timer_fd < 0){ + fprintf(stderr, "Failed to create timer for Lua backend\n"); + return 1; + } + return 0; +} + +static int lua_update_timerfd(){ + uint64_t interval = 0, gcd, residual; + size_t n = 0; + + //find the minimum for the lower interval bounds + for(n = 0; n < timers; n++){ + if(timer[n].interval && (!interval || timer[n].interval < interval)){ + interval = timer[n].interval; + } + } + + //stop the timer + if(!interval){ + memset(&timer_config, 0, sizeof(struct itimerspec)); + } + //calculate gcd of all timers + else{ + for(n = 0; n < timers; n++){ + if(timer[n].interval){ + //calculate gcd of current interval and this timers interval + gcd = timer[n].interval; + while(gcd){ + residual = interval % gcd; + interval = gcd; + gcd = residual; + } + //since we round everything, 10 is the lowest interval we get + if(interval == 10){ + break; + } + } + } + + timer_config.it_interval.tv_sec = interval / 1000; + timer_config.it_interval.tv_nsec = (interval % 1000) * 1e6; + timer_config.it_value.tv_nsec = 1; + } + + //configure the new interval + timerfd_settime(timer_fd, 0, &timer_config, NULL); return 0; } static int lua_callback_output(lua_State* interpreter){ - int arguments = lua_gettop(interpreter); - size_t n; + size_t n = 0; channel_value val; const char* channel_name = NULL; channel* channel = NULL; instance* inst = NULL; lua_instance_data* data = NULL; - if(arguments != 2){ - fprintf(stderr, "Lua output function called with %d arguments, expected 2\n", arguments); + if(lua_gettop(interpreter) != 2){ + fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(interpreter)); return 0; } - channel_name = lua_tostring(interpreter, 1); - val.normalised = clamp(lua_tonumber(interpreter, 2), 1.0, 0.0); - + //get instance pointer from registry lua_pushstring(interpreter, LUA_REGISTRY_KEY); lua_gettable(interpreter, LUA_REGISTRYINDEX); - inst = (instance *) lua_touserdata(interpreter, -1); + inst = (instance*) lua_touserdata(interpreter, -1); data = (lua_instance_data*) inst->impl; + //fetch function parameters + channel_name = lua_tostring(interpreter, 1); + val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); + + //find correct channel & output value for(n = 0; n < data->channels; n++){ if(!strcmp(channel_name, data->channel_name[n])){ channel = mm_channel(inst, n, 0); @@ -56,6 +116,7 @@ static int lua_callback_output(lua_State* interpreter){ return 0; } mm_channel_event(channel, val); + data->output[n] = val.normalised; return 0; } } @@ -64,6 +125,122 @@ static int lua_callback_output(lua_State* interpreter){ return 0; } +static int lua_callback_interval(lua_State* interpreter){ + size_t n = 0; + instance* inst = NULL; + lua_instance_data* data = NULL; + uint64_t interval = 0; + int reference = LUA_NOREF; + + if(lua_gettop(interpreter) != 2){ + fprintf(stderr, "Lua output function called with %d arguments, expected 2 (string, number)\n", lua_gettop(interpreter)); + return 0; + } + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + data = (lua_instance_data*) inst->impl; + + //fetch and round the interval + interval = luaL_checkinteger(interpreter, 2); + if(interval % 10 < 5){ + interval -= interval % 10; + } + else{ + interval += (10 - (interval % 10)); + } + + //push the function again + lua_pushvalue(interpreter, 1); + if(lua_gettable(interpreter, LUA_REGISTRYINDEX) == LUA_TNUMBER){ + //already interval'd + reference = luaL_checkinteger(interpreter, 4); + } + else if(interval){ + //get a reference to the function + lua_pushvalue(interpreter, 1); + reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + + //the function indexes the reference + lua_pushvalue(interpreter, 1); + lua_pushinteger(interpreter, reference); + lua_settable(interpreter, LUA_REGISTRYINDEX); + } + + //find matching timer + for(n = 0; n < timers; n++){ + if(timer[n].reference == reference && timer[n].interpreter == interpreter){ + break; + } + } + + if(n < timers){ + //set new interval + timer[n].interval = interval; + timer[n].delta = 0; + } + else if(interval){ + //append new timer + timer = realloc(timer, (timers + 1) * sizeof(lua_timer)); + if(!timer){ + fprintf(stderr, "Failed to allocate memory\n"); + timers = 0; + return 0; + } + timer[timers].interval = interval; + timer[timers].delta = 0; + timer[timers].interpreter = interpreter; + timer[timers].reference = reference; + timers++; + } + + //recalculate timerspec + lua_update_timerfd(); + return 0; +} + +static int lua_callback_value(lua_State* interpreter, uint8_t input){ + size_t n = 0; + instance* inst = NULL; + lua_instance_data* data = NULL; + const char* channel_name = NULL; + + if(lua_gettop(interpreter) != 1){ + fprintf(stderr, "Lua get_value function called with %d arguments, expected 1 (string)\n", lua_gettop(interpreter)); + return 0; + } + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + data = (lua_instance_data*) inst->impl; + + //fetch argument + channel_name = lua_tostring(interpreter, 1); + + //find correct channel & return value + for(n = 0; n < data->channels; n++){ + if(!strcmp(channel_name, data->channel_name[n])){ + lua_pushnumber(data->interpreter, (input) ? data->input[n] : data->output[n]); + return 1; + } + } + + fprintf(stderr, "Tried to get unknown channel %s.%s\n", inst->name, channel_name); + return 0; +} + +static int lua_callback_input_value(lua_State* interpreter){ + return lua_callback_value(interpreter, 1); +} + +static int lua_callback_output_value(lua_State* interpreter){ + return lua_callback_value(interpreter, 0); +} + static int lua_configure(char* option, char* value){ fprintf(stderr, "The lua backend does not take any global configuration\n"); return 1; @@ -72,6 +249,7 @@ static int lua_configure(char* option, char* value){ static int lua_configure_instance(instance* inst, char* option, char* value){ lua_instance_data* data = (lua_instance_data*) inst->impl; + //load a lua file into the interpreter if(!strcmp(option, "script")){ if(luaL_dofile(data->interpreter, value)){ fprintf(stderr, "Failed to load lua source file %s for instance %s: %s\n", value, inst->name, lua_tostring(data->interpreter, -1)); @@ -105,13 +283,16 @@ static instance* lua_instance(){ } luaL_openlibs(data->interpreter); - //register lua api functions + //register lua interface functions lua_register(data->interpreter, "output", lua_callback_output); + lua_register(data->interpreter, "interval", lua_callback_interval); + lua_register(data->interpreter, "input_value", lua_callback_input_value); + lua_register(data->interpreter, "output_value", lua_callback_output_value); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); lua_pushlightuserdata(data->interpreter, (void *) inst); - lua_settable(data->interpreter, LUA_REGISTRYINDEX); + lua_settable(data->interpreter, LUA_REGISTRYINDEX); inst->impl = data; return inst; @@ -131,11 +312,16 @@ static channel* lua_channel(instance* inst, char* spec){ //allocate new channel if(u == data->channels){ data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*)); - if(!data->channel_name){ + data->reference = realloc(data->reference, (u + 1) * sizeof(int)); + data->input = realloc(data->input, (u + 1) * sizeof(double)); + data->output = realloc(data->output, (u + 1) * sizeof(double)); + if(!data->channel_name || !data->reference || !data->input || !data->output){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; } + data->reference[u] = LUA_NOREF; + data->input[u] = data->output[u] = 0.0; data->channel_name[u] = strdup(spec); if(!data->channel_name[u]){ fprintf(stderr, "Failed to allocate memory\n"); @@ -151,25 +337,91 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t n = 0; lua_instance_data* data = (lua_instance_data*) inst->impl; + //handle all incoming events for(n = 0; n < num; n++){ - //call lua channel handlers - lua_getglobal(data->interpreter, data->channel_name[c[n]->ident]); - lua_pushnumber(data->interpreter, v[n].normalised); - if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ - fprintf(stderr, "Failed to call handler for %s.%s: %s\n", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); - lua_pop(data->interpreter, 1); + data->input[c[n]->ident] = v[n].normalised; + //call lua channel handlers if present + if(data->reference[n] != LUA_NOREF){ + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); + lua_pushnumber(data->interpreter, v[n].normalised); + if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ + fprintf(stderr, "Failed to call handler for %s.%s: %s\n", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); + lua_pop(data->interpreter, 1); + } } } return 0; } static int lua_handle(size_t num, managed_fd* fds){ - //TODO call timer callbacks + uint8_t read_buffer[100]; + uint64_t delta = timer_config.it_interval.tv_sec * 1000 + timer_config.it_interval.tv_nsec / 1e6; + size_t n; + + if(!num){ + return 0; + } + + //read the timer iteration to acknowledge the fd + if(read(timer_fd, read_buffer, sizeof(read_buffer)) < 0){ + fprintf(stderr, "Failed to read from Lua timer: %s\n", strerror(errno)); + return 1; + } + + //add delta to all active timers + for(n = 0; n < timers; n++){ + if(timer[n].interval){ + timer[n].delta += delta; + //call lua function if timer expired + if(timer[n].delta >= timer[n].interval){ + timer[n].delta %= timer[n].interval; + lua_rawgeti(timer[n].interpreter, LUA_REGISTRYINDEX, timer[n].reference); + lua_pcall(timer[n].interpreter, 0, 0, 0); + } + } + } return 0; } static int lua_start(){ - //TODO start timers / register fds + size_t n, u, p; + instance** inst = NULL; + lua_instance_data* data = NULL; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + //resolve channels to their handler functions + for(u = 0; u < n; u++){ + data = (lua_instance_data*) inst[u]->impl; + for(p = 0; p < data->channels; p++){ + //exclude reserved names + if(strcmp(data->channel_name[p], "output") + && strcmp(data->channel_name[p], "input_value") + && strcmp(data->channel_name[p], "output_value") + && strcmp(data->channel_name[p], "interval")){ + lua_getglobal(data->interpreter, data->channel_name[p]); + data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); + if(data->reference[p] == LUA_REFNIL){ + data->reference[p] = LUA_NOREF; + } + } + } + } + + free(inst); + if(!n){ + return 0; + } + + //register the timer with the core + fprintf(stderr, "Lua backend registering 1 descriptor to core\n"); + if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ + return 1; + } return 0; } @@ -178,6 +430,7 @@ static int lua_shutdown(){ instance** inst = NULL; lua_instance_data* data = NULL; + //fetch all instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); return 1; @@ -192,10 +445,19 @@ static int lua_shutdown(){ free(data->channel_name[p]); } free(data->channel_name); + free(data->reference); + free(data->input); + free(data->output); free(inst[u]->impl); } free(inst); + //free module-global data + free(timer); + timer = NULL; + timers = 0; + close(timer_fd); + timer_fd = -1; fprintf(stderr, "Lua backend shut down\n"); return 0; diff --git a/backends/lua.h b/backends/lua.h index 27e1afd..ccffef7 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -17,5 +17,15 @@ static int lua_shutdown(); typedef struct /*_lua_instance_data*/ { size_t channels; char** channel_name; + int* reference; + double* input; + double* output; lua_State* interpreter; } lua_instance_data; + +typedef struct /*_lua_interval_callback*/ { + uint64_t interval; + uint64_t delta; + lua_State* interpreter; + int reference; +} lua_timer; diff --git a/backends/lua.md b/backends/lua.md index 91e8fe2..970e8e2 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -5,15 +5,32 @@ events using the Lua programming language. Every instance has it's own interpreter state which can be loaded with custom handler scripts. -To process incoming channel events, the MIDIMonster calls corresponding Lua functions with -the value (as a Lua `number` type) as parameter. To send output on a channel, the Lua environment -provides the function `output(channel-name, value)`. +To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) +with the value (as a Lua `number` type) as parameter. + +The following functions are provided within the Lua interpreter for interaction with the MIDIMonster + +| Function | Usage example | Description | +|-------------------------------|-------------------------------|---------------------------------------| +| `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel | +| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms) | +| `input_value(string)` | `input_value("foo")` | Get the last input value on a channel | +| `output_value(string)` | `output_value("bar") | Get the last output value on a channel | + Example script: ``` function bar(value) output("foo", value / 2) end + +step = 0 +function toggle() + output("bar", step * 1.0) + step = (step + 1) % 2; +end + +interval(toggle, 1000) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. @@ -41,9 +58,12 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using `output` as an input channel name to a Lua instance does not work, as the interpreter has -`output` globally assigned to the event output function. Using `output` as an output channel name -via `output("output", value)` works as intended. +Using any of the interface functions (`output`, `interval`, `input\_value`, `output\_value`) as an +input channel name to a Lua instance will not call any handler functions. +Using these names as arguments to the output and value interface functions works as intended. + +Output values will not trigger corresponding input event handlers unless the channel is mapped +back in the MIDIMonster configuration. The path to the Lua source files is relative to the current working directory. This may lead to problems when copying configuration between installations. diff --git a/configs/demo.lua b/configs/demo.lua index e816ac4..24a8396 100644 --- a/configs/demo.lua +++ b/configs/demo.lua @@ -1,21 +1,42 @@ --- This example MIDIMonstaer Lua script spreads one input channel onto multiple output +-- This example MIDIMonster Lua script spreads one input channel onto multiple output -- channels using a polynomial function evaluated at multiple points. This effect can -- be visualized e.g. with martrix (https://github.com/cbdevnet/martrix). --- This is just a demonstration of global variables -foo = 0 - -- The polynomial to evaluate -function polynomial(offset, x) - return math.exp(-20 * (x - offset) ^ 2) +function polynomial(x) + return math.exp(-40 * input_value("width") * (x - input_value("offset")) ^ 2) end --- Handler function for the input channel -function input(value) - foo = foo + 1 - print("input at ", value, foo) - +-- Evaluate and set output channels +function evaluate() for chan=0,10 do - output("out" .. chan, polynomial(value, (1 / 10) * chan)) + output("out" .. chan, polynomial((1 / 10) * chan)) + end +end + +-- Handler functions for the input channels +function offset(value) + evaluate() +end + +function width(value) + evaluate() +end + +-- This is an example showing a simple chase running on its own without the need +-- (but the possibility) for external input + +-- Global value for the current step +current_step = 0 + +function step() + if(current_step > 0) then + output("dim", 0.0) + else + output("dim", 1.0) end + + current_step = (current_step + 1) % 2 end + +interval(step, 1000) diff --git a/configs/lua.cfg b/configs/lua.cfg index 9182122..42fd27d 100644 --- a/configs/lua.cfg +++ b/configs/lua.cfg @@ -8,8 +8,9 @@ bind = 0.0.0.0 device = /dev/input/by-path/platform-i8042-serio-2-event-mouse [evdev xbox] -input = Xbox Wireless +device = /dev/input/event17 axis.ABS_X = 34300 0 65535 255 4095 +axis.ABS_Y = 34300 0 65535 255 4095 [lua lua] script = configs/demo.lua @@ -19,8 +20,9 @@ universe = 0 destination = 255.255.255.255 [map] -mouse.EV_KEY.BTN_LEFT > lua.input -xbox.EV_ABS.ABS_X > lua.input +mouse.EV_KEY.BTN_LEFT > lua.click +xbox.EV_ABS.ABS_X > lua.offset +xbox.EV_ABS.ABS_Y > lua.width art.1 < lua.out0 art.2 < lua.out1 @@ -33,3 +35,5 @@ art.8 < lua.out7 art.9 < lua.out8 art.10 < lua.out9 art.11 < lua.out10 + +art.12 < lua.dim -- cgit v1.2.3 From c517b26ac559356650334a2d32d1b6249e66289d Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 13 Jul 2019 17:56:23 +0200 Subject: Fix indexing bug --- backends/lua.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/lua.c b/backends/lua.c index 08d8d3c..7f0c2de 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -341,7 +341,7 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ for(n = 0; n < num; n++){ data->input[c[n]->ident] = v[n].normalised; //call lua channel handlers if present - if(data->reference[n] != LUA_NOREF){ + if(data->reference[c[n]->ident] != LUA_NOREF){ lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); lua_pushnumber(data->interpreter, v[n].normalised); if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ -- cgit v1.2.3 From f19d6e66b23ba719f474171b10e1ee294fb38d55 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 13 Jul 2019 18:13:36 +0200 Subject: Fix & simplify timer configuration --- backends/lua.c | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index 7f0c2de..8069f8f 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -9,9 +9,7 @@ static size_t timers = 0; static lua_timer* timer = NULL; -static struct itimerspec timer_config = { - 0 -}; +uint64_t timer_interval = 0; static int timer_fd = -1; int init(){ @@ -43,8 +41,11 @@ int init(){ } static int lua_update_timerfd(){ - uint64_t interval = 0, gcd, residual; + uint64_t interval, gcd, residual; size_t n = 0; + struct itimerspec timer_config = { + 0 + }; //find the minimum for the lower interval bounds for(n = 0; n < timers; n++){ @@ -53,12 +54,8 @@ static int lua_update_timerfd(){ } } - //stop the timer - if(!interval){ - memset(&timer_config, 0, sizeof(struct itimerspec)); - } - //calculate gcd of all timers - else{ + //calculate gcd of all timers if any are active + if(interval){ for(n = 0; n < timers; n++){ if(timer[n].interval){ //calculate gcd of current interval and this timers interval @@ -75,13 +72,17 @@ static int lua_update_timerfd(){ } } - timer_config.it_interval.tv_sec = interval / 1000; - timer_config.it_interval.tv_nsec = (interval % 1000) * 1e6; - timer_config.it_value.tv_nsec = 1; + timer_config.it_interval.tv_sec = timer_config.it_value.tv_sec = interval / 1000; + timer_config.it_interval.tv_nsec = timer_config.it_value.tv_nsec = (interval % 1000) * 1e6; + } + + if(interval == timer_interval){ + return 0; } //configure the new interval timerfd_settime(timer_fd, 0, &timer_config, NULL); + timer_interval = interval; return 0; } @@ -355,7 +356,7 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ static int lua_handle(size_t num, managed_fd* fds){ uint8_t read_buffer[100]; - uint64_t delta = timer_config.it_interval.tv_sec * 1000 + timer_config.it_interval.tv_nsec / 1e6; + uint64_t delta = timer_interval; size_t n; if(!num){ -- cgit v1.2.3 From e776e02531aca45f424f7139e5d7304ba3096b45 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 14 Jul 2019 16:49:31 +0200 Subject: Work around missing timerfd on OSX/Windows --- backend.c | 2 +- backends/lua.c | 40 ++++++++++++++++++++++++++++++++++++++++ backends/lua.h | 6 ++++++ backends/lua.md | 4 ++-- configs/lua.cfg | 2 +- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/backend.c b/backend.c index ed5e6db..5df5d73 100644 --- a/backend.c +++ b/backend.c @@ -227,7 +227,7 @@ struct timeval backend_timeout(){ struct timeval tv = { secs, - msecs + msecs * 1000 }; return tv; } diff --git a/backends/lua.c b/backends/lua.c index 8069f8f..7fcd0ab 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -10,10 +10,17 @@ 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 int init(){ backend lua = { + #ifndef MMBACKEND_LUA_TIMERFD + .interval = lua_interval, + #endif .name = BACKEND_NAME, .conf = lua_configure, .create = lua_instance, @@ -31,21 +38,33 @@ int init(){ return 1; } + #ifdef MMBACKEND_LUA_TIMERFD //create the timer to expire intervals timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); if(timer_fd < 0){ fprintf(stderr, "Failed to create timer for Lua backend\n"); return 1; } + #endif return 0; } +static uint32_t lua_interval(){ + //FIXME Return delta for next timer here + if(timer_interval){ + return timer_interval; + } + return 1000; +} + static int lua_update_timerfd(){ uint64_t interval, gcd, residual; size_t n = 0; + #ifdef MMBACKEND_LUA_TIMERFD struct itimerspec timer_config = { 0 }; + #endif //find the minimum for the lower interval bounds for(n = 0; n < timers; n++){ @@ -72,16 +91,20 @@ static int lua_update_timerfd(){ } } + #ifdef MMBACKEND_LUA_TIMERFD timer_config.it_interval.tv_sec = timer_config.it_value.tv_sec = interval / 1000; timer_config.it_interval.tv_nsec = timer_config.it_value.tv_nsec = (interval % 1000) * 1e6; + #endif } if(interval == timer_interval){ return 0; } + #ifdef MMBACKEND_LUA_TIMERFD //configure the new interval timerfd_settime(timer_fd, 0, &timer_config, NULL); + #endif timer_interval = interval; return 0; } @@ -359,6 +382,7 @@ static int lua_handle(size_t num, managed_fd* fds){ uint64_t delta = timer_interval; size_t n; + #ifdef MMBACKEND_LUA_TIMERFD if(!num){ return 0; } @@ -368,6 +392,18 @@ static int lua_handle(size_t num, managed_fd* fds){ fprintf(stderr, "Failed to read from Lua timer: %s\n", strerror(errno)); return 1; } + #else + if(!last_timestamp){ + last_timestamp = mm_timestamp(); + } + delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); + #endif + + //no timers active + if(!timer_interval){ + return 0; + } //add delta to all active timers for(n = 0; n < timers; n++){ @@ -418,11 +454,13 @@ static int lua_start(){ return 0; } + #ifdef MMBACKEND_LUA_TIMERFD //register the timer with the core fprintf(stderr, "Lua backend registering 1 descriptor to core\n"); if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ return 1; } + #endif return 0; } @@ -457,8 +495,10 @@ static int lua_shutdown(){ free(timer); timer = NULL; timers = 0; + #ifdef MMBACKEND_LUA_TIMERFD close(timer_fd); timer_fd = -1; + #endif fprintf(stderr, "Lua backend shut down\n"); return 0; diff --git a/backends/lua.h b/backends/lua.h index ccffef7..7aad891 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -4,6 +4,11 @@ #include #include +//OSX and Windows don't have the cool new toys... +#ifdef __linux__ + #define MMBACKEND_LUA_TIMERFD +#endif + int init(); static int lua_configure(char* option, char* value); static int lua_configure_instance(instance* inst, char* option, char* value); @@ -13,6 +18,7 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v); static int lua_handle(size_t num, managed_fd* fds); static int lua_start(); static int lua_shutdown(); +static uint32_t lua_interval(); typedef struct /*_lua_instance_data*/ { size_t channels; diff --git a/backends/lua.md b/backends/lua.md index 970e8e2..e273b28 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -15,7 +15,7 @@ The following functions are provided within the Lua interpreter for interaction | `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel | | `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms) | | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel | -| `output_value(string)` | `output_value("bar") | Get the last output value on a channel | +| `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | Example script: @@ -58,7 +58,7 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using any of the interface functions (`output`, `interval`, `input\_value`, `output\_value`) as an +Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`) as an input channel name to a Lua instance will not call any handler functions. Using these names as arguments to the output and value interface functions works as intended. diff --git a/configs/lua.cfg b/configs/lua.cfg index 42fd27d..b892e91 100644 --- a/configs/lua.cfg +++ b/configs/lua.cfg @@ -8,7 +8,7 @@ bind = 0.0.0.0 device = /dev/input/by-path/platform-i8042-serio-2-event-mouse [evdev xbox] -device = /dev/input/event17 +;device = /dev/input/event17 axis.ABS_X = 34300 0 65535 255 4095 axis.ABS_Y = 34300 0 65535 255 4095 -- cgit v1.2.3 From f4f6f8c84a685b83f2406f8efb148b6699be14e9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 14 Jul 2019 16:51:40 +0200 Subject: Conditionally include timerfd headers --- backends/lua.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backends/lua.c b/backends/lua.c index 7fcd0ab..43a6fcc 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -1,7 +1,10 @@ #include -#include #include #include +#ifdef MMBACKEND_LUA_TIMERFD +#include +#endif + #include "lua.h" #define BACKEND_NAME "lua" -- cgit v1.2.3 From 99f54f6d15d9e286fb0b47cf21b32318767d3c2a Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 14 Jul 2019 16:55:19 +0200 Subject: Fix header ordering --- backends/lua.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index 43a6fcc..8e221d1 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -1,3 +1,5 @@ +#include "lua.h" + #include #include #include @@ -5,8 +7,6 @@ #include #endif -#include "lua.h" - #define BACKEND_NAME "lua" #define LUA_REGISTRY_KEY "_midimonster_lua_instance" -- cgit v1.2.3 From 5bd8e81e2821f1378c6773fbc1f06df063dbbd22 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 20 Jul 2019 18:10:56 +0200 Subject: Implement multi-channel mapping syntax --- README.md | 30 ++++++- TODO | 2 +- config.c | 196 +++++++++++++++++++++++++++++++++++++++++---- configs/launchctl-sacn.cfg | 25 ++---- midimonster.h | 53 +++++++++++- monster.cfg | 46 ++--------- 6 files changed, 274 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index ee907a6..bce7b71 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,24 @@ lines of the form `option = value`. Lines starting with a semicolon are treated as comments and ignored. Inline comments are not currently supported. +Example configuration files may be found in [configs/](configs/). + +### Backend and instance configuration + A configuration section may either be a *backend configuration* section, started by `[backend ]`, an *instance configuration* section, started by `[ ]` or a *mapping* section started by `[map]`. +Backends document their global options in their [backend documentation](#backend-documentation). +Some backends may not require global configuration, in which case the configuration +section for that particular backend can be omitted. + +To make an instance available for mapping channels, it requires at least the +`[ ]` configuration stanza. Most backends require +additional configuration for their instances. + +### Channel mapping + The `[map]` section consists of lines of channel-to-channel assignments, reading like ``` @@ -76,7 +90,21 @@ output eachothers events. The last line is a shorter way to create a bi-directional mapping. -Example configuration files may be found in [configs/](configs/). +### Multi-channel mapping + +To make mapping large contiguous sets of channels easier, channel names may contain +expressions of the form `{..}`, with *start* and *end* being positive integers +delimiting a range of channels. Multiple such expressions may be used in one channel +specification, with the rightmost expression being incremented (or decremented) first for +evaluation. + +Both sides of a multi-channel assignment need to have the same number of channels. + +Example multi-channel mapping: + +``` +instance-a.channel{1..10} > instance-b.{10..1} +``` ## Backend documentation diff --git a/TODO b/TODO index f39dae0..76f2c4e 100644 --- a/TODO +++ b/TODO @@ -4,5 +4,5 @@ Optimize core channel search (store backend offset) Printing backend document example configs -lua timer evdev relative axis size +mm_managed_fd.impl is not freed currently diff --git a/config.c b/config.c index b81aeaf..24f1223 100644 --- a/config.c +++ b/config.c @@ -21,25 +21,164 @@ static backend* current_backend = NULL; static instance* current_instance = NULL; static char* config_trim_line(char* in){ - ssize_t u; + ssize_t n; //trim front for(; *in && !isgraph(*in); in++){ } //trim back - for(u = strlen(in); u >= 0 && !isgraph(in[u]); u--){ - in[u] = 0; + for(n = strlen(in); n >= 0 && !isgraph(in[n]); n--){ + in[n] = 0; } return in; } +static int config_glob_parse(channel_glob* glob, char* spec, size_t length){ + char* parse_offset = NULL; + //FIXME might want to allow negative delimiters at some point + + //first interval member + glob->limits.u64[0] = strtoul(spec, &parse_offset, 10); + if(!parse_offset || parse_offset - spec >= length || strncmp(parse_offset, "..", 2)){ + return 1; + } + + parse_offset += 2; + //second interval member + glob->limits.u64[1] = strtoul(parse_offset, &parse_offset, 10); + if(!parse_offset || parse_offset - spec != length || *parse_offset != '}'){ + return 1; + } + + //calculate number of channels within interval + if(glob->limits.u64[0] < glob->limits.u64[1]){ + glob->values = glob->limits.u64[1] - glob->limits.u64[0] + 1; + } + else if(glob->limits.u64[0] > glob->limits.u64[1]){ + glob->values = glob->limits.u64[0] - glob->limits.u64[1] + 1; + } + else{ + glob->values = 1; + } + + return 0; +} + +static int config_glob_scan(instance* inst, channel_spec* spec){ + char* glob_start = spec->spec, *glob_end = NULL; + size_t u; + + //assume a spec is one channel as default + spec->channels = 1; + + //scan and mark globs + for(glob_start = strchr(glob_start, '{'); glob_start; glob_start = strchr(glob_start, '{')){ + glob_end = strchr(glob_start, '}'); + if(!glob_end){ + fprintf(stderr, "Failed to parse channel spec, unterminated glob: %s\n", spec->spec); + return 1; + } + + spec->glob = realloc(spec->glob, (spec->globs + 1) * sizeof(channel_glob)); + if(!spec->glob){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + spec->glob[spec->globs].offset[0] = glob_start - spec->spec; + spec->glob[spec->globs].offset[1] = glob_end - spec->spec; + spec->globs++; + + //skip this opening brace + glob_start++; + } + + //try to parse globs internally + spec->internal = 1; + for(u = 0; u < spec->globs; u++){ + if(config_glob_parse(spec->glob + u, + spec->spec + spec->glob[u].offset[0] + 1, + spec->glob[u].offset[1] - spec->glob[u].offset[0] - 1)){ + spec->internal = 0; + break; + } + } + if(!spec->internal){ + //TODO try to parse globs externally + fprintf(stderr, "Failed to parse glob %lu in %s internally\n", u + 1, spec->spec); + return 1; + } + + //calculate channel total + for(u = 0; u < spec->globs; u++){ + spec->channels *= spec->glob[u].values; + } + return 0; +} + +static channel* config_glob_resolve(instance* inst, channel_spec* spec, uint64_t n){ + size_t glob = 0, glob_length; + ssize_t bytes = 0; + uint64_t current_value = 0; + channel* result = NULL; + char* resolved_spec = strdup(spec->spec); + + if(!resolved_spec){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + //TODO if not internal, try to resolve externally + + //iterate and resolve globs + for(glob = spec->globs; glob > 0; glob--){ + current_value = spec->glob[glob - 1].limits.u64[0] + (n % spec->glob[glob - 1].values); + if(spec->glob[glob - 1].limits.u64[0] > spec->glob[glob - 1].limits.u64[1]){ + current_value = spec->glob[glob - 1].limits.u64[0] - (n % spec->glob[glob - 1].values); + } + glob_length = spec->glob[glob - 1].offset[1] - spec->glob[glob - 1].offset[0]; + n /= spec->glob[glob - 1].values; + + //write out value + bytes = snprintf(resolved_spec + spec->glob[glob - 1].offset[0], + glob_length, + "%lu", + current_value); + if(bytes > glob_length){ + fprintf(stderr, "Internal error resolving glob %s\n", spec->spec); + goto bail; + } + + //move trailing data + if(bytes < glob_length){ + memmove(resolved_spec + spec->glob[glob - 1].offset[0] + bytes, + resolved_spec + spec->glob[glob - 1].offset[1] + 1, + strlen(spec->spec) - spec->glob[glob - 1].offset[1]); + } + } + + result = inst->backend->channel(inst, resolved_spec); + if(spec->globs && !result){ + fprintf(stderr, "Failed to match multichannel evaluation %s to a channel\n", resolved_spec); + } + +bail: + free(resolved_spec); + return result; +} + static int config_map(char* to_raw, char* from_raw){ //create a copy because the original pointer may be used multiple times char* to = strdup(to_raw), *from = strdup(from_raw); - char* chanspec_to = to, *chanspec_from = from; + channel_spec spec_to = { + .spec = to + }, spec_from = { + .spec = from + }; instance* instance_to = NULL, *instance_from = NULL; channel* channel_from = NULL, *channel_to = NULL; + uint64_t n = 0; int rv = 1; if(!from || !to){ @@ -50,21 +189,21 @@ static int config_map(char* to_raw, char* from_raw){ } //separate channel spec from instance - for(; *chanspec_to && *chanspec_to != '.'; chanspec_to++){ + for(; *(spec_to.spec) && *(spec_to.spec) != '.'; spec_to.spec++){ } - for(; *chanspec_from && *chanspec_from != '.'; chanspec_from++){ + for(; *(spec_from.spec) && *(spec_from.spec) != '.'; spec_from.spec++){ } - if(!*chanspec_to || !*chanspec_from){ + if(!spec_from.spec[0] || !spec_to.spec[0]){ fprintf(stderr, "Mapping does not contain a proper instance specification\n"); goto done; } //terminate - *chanspec_to = *chanspec_from = 0; - chanspec_to++; - chanspec_from++; + spec_from.spec[0] = spec_to.spec[0] = 0; + spec_from.spec++; + spec_to.spec++; //find matching instances instance_to = instance_match(to); @@ -75,16 +214,41 @@ static int config_map(char* to_raw, char* from_raw){ goto done; } - channel_from = instance_from->backend->channel(instance_from, chanspec_from); - channel_to = instance_to->backend->channel(instance_to, chanspec_to); - - if(!channel_from || !channel_to){ - fprintf(stderr, "Failed to parse channel specifications\n"); + //scan for globs + if(config_glob_scan(instance_to, &spec_to) + || config_glob_scan(instance_from, &spec_from)){ goto done; } + + if(spec_to.channels != spec_from.channels + || spec_to.channels == 0 + || spec_from.channels == 0){ + fprintf(stderr, "Multi-channel specification size mismatch: %s.%s (%lu channels) - %s.%s (%lu channels)\n", + instance_from->name, + spec_from.spec, + spec_from.channels, + instance_to->name, + spec_to.spec, + spec_to.channels); + goto done; + } + + //iterate, resolve globs and map + rv = 0; + for(n = 0; !rv && n < spec_from.channels; n++){ + channel_from = config_glob_resolve(instance_from, &spec_from, n); + channel_to = config_glob_resolve(instance_to, &spec_to, n); + + if(!channel_from || !channel_to){ + rv = 1; + goto done; + } + rv |= mm_map_channel(channel_from, channel_to); + } - rv = mm_map_channel(channel_from, channel_to); done: + free(spec_from.glob); + free(spec_to.glob); free(from); free(to); return rv; diff --git a/configs/launchctl-sacn.cfg b/configs/launchctl-sacn.cfg index c2dec84..02cd152 100644 --- a/configs/launchctl-sacn.cfg +++ b/configs/launchctl-sacn.cfg @@ -6,34 +6,19 @@ [backend midi] name = MIDIMonster -[backend sacn] +[backend artnet] bind = 0.0.0.0 [midi lc] read = Launch Control -[sacn out] -universe = 1 -priority = 100 +[artnet out] +universe = 0 +destination = 255.255.255.255 [map] -lc.ch0.cc0 > out.1 -lc.ch0.cc1 > out.2 -lc.ch0.cc2 > out.3 -lc.ch0.cc3 > out.4 -lc.ch0.cc4 > out.5 -lc.ch0.cc5 > out.6 -lc.ch0.cc6 > out.7 -lc.ch0.cc7 > out.8 -lc.ch0.cc8 > out.9 -lc.ch0.cc9 > out.10 -lc.ch0.cc10 > out.11 -lc.ch0.cc11 > out.12 -lc.ch0.cc12 > out.13 -lc.ch0.cc13 > out.14 -lc.ch0.cc14 > out.15 -lc.ch0.cc15 > out.16 +lc.ch0.cc{0..15} > out.{1..16} lc.ch0.note0 > out.1 lc.ch0.note1 > out.2 diff --git a/midimonster.h b/midimonster.h index 8a18155..1fc85b7 100644 --- a/midimonster.h +++ b/midimonster.h @@ -3,9 +3,15 @@ #include #include #include + +/* Straight-forward min / max macros */ #define max(a,b) (((a) > (b)) ? (a) : (b)) #define min(a,b) (((a) < (b)) ? (a) : (b)) + +/* Clamp a value to a range */ #define clamp(val,max,min) (((val) > (max)) ? (max) : (((val) < (min)) ? (min) : (val))) + +/* Debug messages only compile in when DEBUG is set */ #ifdef DEBUG #define DBGPF(format, ...) fprintf(stderr, (format), __VA_ARGS__) #define DBG(message) fprintf(stderr, "%s", (message)) @@ -14,10 +20,13 @@ #define DBG(message) #endif +/* Pull in additional defines for non-linux platforms */ #include "portability.h" +/* Default configuration file name to read when no other is specified */ #define DEFAULT_CFG "monster.cfg" +/* Forward declare some of the structs so we can use them in eachother */ struct _channel_value; struct _backend_channel; struct _backend_instance; @@ -86,6 +95,7 @@ typedef int (*mmbackend_start)(); typedef uint32_t (*mmbackend_interval)(); typedef int (*mmbackend_shutdown)(); +/* Channel event value, .normalised is used by backends to determine channel values */ typedef struct _channel_value { union { double dbl; @@ -94,6 +104,10 @@ typedef struct _channel_value { double normalised; } channel_value; +/* + * Backend callback structure + * Used to register a backend with the core using mm_backend_register() + */ typedef struct /*_mm_backend*/ { char* name; mmbackend_configure conf; @@ -108,6 +122,10 @@ typedef struct /*_mm_backend*/ { mmbackend_interval interval; } backend; +/* + * Backend instance structure - do not allocate directly! + * Use the memory returned by mm_instance() + */ typedef struct _backend_instance { backend* backend; uint64_t ident; @@ -115,20 +133,51 @@ typedef struct _backend_instance { char* name; } instance; +/* + * Channel specification glob + */ +typedef struct /*_mm_channel_glob*/ { + size_t offset[2]; + union { + void* impl; + uint64_t u64[2]; + } limits; + uint64_t values; +} channel_glob; + +/* + * (Multi-)Channel specification + */ +typedef struct /*_mm_channel_spec*/ { + char* spec; + uint8_t internal; + size_t channels; + size_t globs; + channel_glob* glob; +} channel_spec; + +/* + * Instance channel structure + * Backends may either manage their own channel registry + * or use the memory returned by mm_channel() + */ typedef struct _backend_channel { instance* instance; uint64_t ident; void* impl; } channel; -//FIXME might be replaced by struct pollfd -//FIXME who frees impl +/* + * File descriptor management structure + * Register for the core event loop using mm_manage_fd() + */ typedef struct _managed_fd { int fd; backend* backend; void* impl; } managed_fd; +/* Internal channel mapping structure - Core use only */ typedef struct /*_mm_channel_mapping*/ { channel* from; size_t destinations; diff --git a/monster.cfg b/monster.cfg index 0760571..7db3ec3 100644 --- a/monster.cfg +++ b/monster.cfg @@ -1,46 +1,16 @@ -[backend sacn] -name = sACN source -bind = 0.0.0.0 - [backend artnet] bind = 0.0.0.0 -[backend ola] - [artnet art] -universe = 1 -dest = 129.13.215.0 - -[backend midi] -name = Monster - -;[evdev in] -;input = Xbox Wireless Controller - -[sacn sacn] -universe = 1 -priority = 100 +universe = 0 +dest = 255.255.255.255 -[midi midi] -read = Axiom +[evdev mouse] +input = TPPS -;[ola ola] +[loopback loop] [map] -;in.EV_ABS.ABS_X > sacn.1+2 -;in.EV_ABS.ABS_Y > sacn.3 -;in.EV_ABS.ABS_X > art.1+2 -;in.EV_ABS.ABS_Y > art.3 -;in.EV_ABS.ABS_X > ola.1+2 -;in.EV_ABS.ABS_Y > ola.3 -;in.EV_KEY.BTN_THUMBL > sacn.4 -;in.EV_KEY.BTN_THUMBR > sacn.5 -;in.EV_ABS.ABS_GAS > sacn.6+7 -;in.EV_ABS.ABS_BRAKE > sacn.8 -;ola.1 > midi.cc0.1 -;ola.2+3 > midi.cc0.2 -midi.ch0.pitch > midi.ch0.cc1 -midi.ch0.aftertouch > midi.ch0.cc2 -midi.ch0.cc71 > midi.ch0.pitch -midi.ch0.cc74 > midi.ch0.aftertouch -midi.ch0.cc91 > midi.ch0.pressure1 +art.{3..4}{4..3} > loop.chan{4..3}{3..4} +art.{1..10} > loop.data{1..10} +art.{500..599} > loop.test{500..599} -- cgit v1.2.3 From 59857ead2d439d450afc6f5144c9c08e8d0c8a5c Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 24 Jul 2019 21:12:01 +0200 Subject: Fix issues found by Coverity Scan --- backends/lua.c | 10 +++++----- midimonster.h | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index 8e221d1..4d946cd 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -61,7 +61,7 @@ static uint32_t lua_interval(){ } static int lua_update_timerfd(){ - uint64_t interval, gcd, residual; + uint64_t interval = 0, gcd, residual; size_t n = 0; #ifdef MMBACKEND_LUA_TIMERFD struct itimerspec timer_config = { @@ -396,11 +396,11 @@ static int lua_handle(size_t num, managed_fd* fds){ return 1; } #else - if(!last_timestamp){ - last_timestamp = mm_timestamp(); - } - delta = mm_timestamp() - last_timestamp; + if(!last_timestamp){ last_timestamp = mm_timestamp(); + } + delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); #endif //no timers active diff --git a/midimonster.h b/midimonster.h index 1fc85b7..7f70f5b 100644 --- a/midimonster.h +++ b/midimonster.h @@ -26,7 +26,7 @@ /* Default configuration file name to read when no other is specified */ #define DEFAULT_CFG "monster.cfg" -/* Forward declare some of the structs so we can use them in eachother */ +/* Forward declare some of the structs so we can use them in each other */ struct _channel_value; struct _backend_channel; struct _backend_instance; -- cgit v1.2.3 From 1f0de6d91c14217ae3893da0e4b6089b799ed026 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 24 Jul 2019 21:30:40 +0200 Subject: Update example configurations to new syntax, add comments --- TODO | 2 +- configs/evdev.cfg | 32 ++++++++ configs/evdev.conf | 31 -------- configs/launchctl-sacn.cfg | 10 +-- configs/lua.cfg | 13 +-- configs/midi-osc.cfg | 25 ++---- configs/osc-artnet.cfg | 51 +----------- configs/osc-kbd.cfg | 16 +--- configs/unifest-17.cfg | 191 +++++++++------------------------------------ 9 files changed, 82 insertions(+), 289 deletions(-) create mode 100644 configs/evdev.cfg delete mode 100644 configs/evdev.conf diff --git a/TODO b/TODO index 76f2c4e..d114640 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,7 @@ MIDI NRPN Note source in channel value struct Optimize core channel search (store backend offset) -Printing backend +Printing backend / Verbose mode document example configs evdev relative axis size diff --git a/configs/evdev.cfg b/configs/evdev.cfg new file mode 100644 index 0000000..bb27caf --- /dev/null +++ b/configs/evdev.cfg @@ -0,0 +1,32 @@ +; Map the (admittedly weird) bluetooth profile of an Xbox One +; Gamepad to some ArtNet output channels. Uses both analog joysticks +; and the analog triggers. + +[backend artnet] +bind = 0.0.0.0 6454 +net = 0 + +[evdev xbox] +device = /dev/input/event14 + +[artnet out] +uni = 0 +dest = 255.255.255.255 + +[map] +xbox.EV_ABS.ABS_X > out.1+2 +xbox.EV_ABS.ABS_Y > out.3+4 + +xbox.EV_ABS.ABS_Z > out.16+17 +xbox.EV_ABS.ABS_RZ > out.18+19 + +xbox.EV_ABS.ABS_BRAKE > out.8 +xbox.EV_ABS.ABS_GAS > out.23 + +xbox.EV_KEY.BTN_NORTH > out.5 +xbox.EV_KEY.BTN_EAST > out.6 +xbox.EV_KEY.BTN_SOUTH > out.7 + +xbox.EV_KEY.BTN_NORTH > out.20 +xbox.EV_KEY.BTN_EAST > out.21 +xbox.EV_KEY.BTN_SOUTH > out.22 diff --git a/configs/evdev.conf b/configs/evdev.conf deleted file mode 100644 index 386e154..0000000 --- a/configs/evdev.conf +++ /dev/null @@ -1,31 +0,0 @@ -[backend artnet] -bind = * 6454 -net = 0 - -[evdev xbox] -device = /dev/input/event14 -axis.ABS_X = 34300 0 65535 255 4095 -axis.ABS_RZ = 34300 0 65535 255 4095 -axis.ABS_Y = 34300 0 65535 255 4095 - -[artnet out] -uni = 0 -dest = 129.13.215.127 - -[map] -xbox.EV_ABS.ABS_X > out.1+2 -xbox.EV_ABS.ABS_Y > out.3+4 - -xbox.EV_ABS.ABS_Z > out.16+17 -xbox.EV_ABS.ABS_RZ > out.18+19 - -xbox.EV_ABS.ABS_BRAKE > out.8 -xbox.EV_ABS.ABS_GAS > out.23 - -xbox.EV_KEY.BTN_NORTH > out.5 -xbox.EV_KEY.BTN_EAST > out.6 -xbox.EV_KEY.BTN_SOUTH > out.7 - -xbox.EV_KEY.BTN_NORTH > out.20 -xbox.EV_KEY.BTN_EAST > out.21 -xbox.EV_KEY.BTN_SOUTH > out.22 diff --git a/configs/launchctl-sacn.cfg b/configs/launchctl-sacn.cfg index 02cd152..781ca40 100644 --- a/configs/launchctl-sacn.cfg +++ b/configs/launchctl-sacn.cfg @@ -19,12 +19,4 @@ destination = 255.255.255.255 [map] lc.ch0.cc{0..15} > out.{1..16} - -lc.ch0.note0 > out.1 -lc.ch0.note1 > out.2 -lc.ch0.note2 > out.3 -lc.ch0.note3 > out.4 -lc.ch0.note4 > out.5 -lc.ch0.note5 > out.6 -lc.ch0.note6 > out.7 -lc.ch0.note7 > out.8 +lc.ch0.note{0..7} > out.{1..8} diff --git a/configs/lua.cfg b/configs/lua.cfg index b892e91..af17496 100644 --- a/configs/lua.cfg +++ b/configs/lua.cfg @@ -24,16 +24,5 @@ mouse.EV_KEY.BTN_LEFT > lua.click xbox.EV_ABS.ABS_X > lua.offset xbox.EV_ABS.ABS_Y > lua.width -art.1 < lua.out0 -art.2 < lua.out1 -art.3 < lua.out2 -art.4 < lua.out3 -art.5 < lua.out4 -art.6 < lua.out5 -art.7 < lua.out6 -art.8 < lua.out7 -art.9 < lua.out8 -art.10 < lua.out9 -art.11 < lua.out10 - +art.{1..11} < lua.out{0..10} art.12 < lua.dim diff --git a/configs/midi-osc.cfg b/configs/midi-osc.cfg index 755077c..1b3ccd6 100644 --- a/configs/midi-osc.cfg +++ b/configs/midi-osc.cfg @@ -1,14 +1,18 @@ +; Translate a MIDI fader wing into an OSC fader view and vice versa + [backend midi] name = MIDIMonster [backend artnet] -bind = * 6454 +bind = 0.0.0.0 6454 net = 0 [osc touch] bind = * 8000 dest = learn@8000 root = /4 + +; Pre-declare the fader values so the range mapping is correct /fader1 = f 0.0 1.0 /fader2 = f 0.0 1.0 /fader3 = f 0.0 1.0 @@ -33,20 +37,5 @@ write = BCF [map] -bcf.ch0.cc81 <> touch./fader1 -bcf.ch0.cc82 <> touch./fader2 -bcf.ch0.cc83 <> touch./fader3 -bcf.ch0.cc84 <> touch./fader4 -bcf.ch0.cc85 <> touch./fader5 -bcf.ch0.cc86 <> touch./fader6 -bcf.ch0.cc87 <> touch./fader7 -bcf.ch0.cc88 <> touch./fader8 - -bcf.ch0.cc81 <> touch./multifader1/1 -bcf.ch0.cc82 <> touch./multifader1/2 -bcf.ch0.cc83 <> touch./multifader1/3 -bcf.ch0.cc84 <> touch./multifader1/4 -bcf.ch0.cc85 <> touch./multifader1/5 -bcf.ch0.cc86 <> touch./multifader1/6 -bcf.ch0.cc87 <> touch./multifader1/7 -bcf.ch0.cc88 <> touch./multifader1/8 +bcf.ch0.cc{81..88} <> touch./fader{1..8} +bcf.ch0.cc{81..88} <> touch./multifader1/{1..8} diff --git a/configs/osc-artnet.cfg b/configs/osc-artnet.cfg index 3eeba2e..ab1d767 100644 --- a/configs/osc-artnet.cfg +++ b/configs/osc-artnet.cfg @@ -12,52 +12,5 @@ dest = learn@8001 destination = 255.255.255.255 [map] -touch./4/multifader1/1 > out.1 -touch./4/multifader1/2 > out.2 -touch./4/multifader1/3 > out.3 -touch./4/multifader1/4 > out.4 -touch./4/multifader1/5 > out.5 -touch./4/multifader1/6 > out.6 -touch./4/multifader1/7 > out.7 -touch./4/multifader1/8 > out.8 -touch./4/multifader1/9 > out.9 -touch./4/multifader1/10 > out.10 -touch./4/multifader1/11 > out.11 -touch./4/multifader1/12 > out.12 -touch./4/multifader1/13 > out.13 -touch./4/multifader1/14 > out.14 -touch./4/multifader1/15 > out.15 -touch./4/multifader1/16 > out.16 -touch./4/multifader1/17 > out.17 -touch./4/multifader1/18 > out.18 -touch./4/multifader1/19 > out.19 -touch./4/multifader1/20 > out.20 -touch./4/multifader1/21 > out.21 -touch./4/multifader1/22 > out.22 -touch./4/multifader1/23 > out.23 -touch./4/multifader1/24 > out.24 - -touch./4/multifader2/1 > out.25 -touch./4/multifader2/2 > out.26 -touch./4/multifader2/3 > out.27 -touch./4/multifader2/4 > out.28 -touch./4/multifader2/5 > out.29 -touch./4/multifader2/6 > out.30 -touch./4/multifader2/7 > out.31 -touch./4/multifader2/8 > out.32 -touch./4/multifader2/9 > out.33 -touch./4/multifader2/10 > out.34 -touch./4/multifader2/11 > out.35 -touch./4/multifader2/12 > out.36 -touch./4/multifader2/13 > out.37 -touch./4/multifader2/14 > out.38 -touch./4/multifader2/15 > out.39 -touch./4/multifader2/16 > out.40 -touch./4/multifader2/17 > out.41 -touch./4/multifader2/18 > out.42 -touch./4/multifader2/19 > out.43 -touch./4/multifader2/20 > out.44 -touch./4/multifader2/21 > out.45 -touch./4/multifader2/22 > out.46 -touch./4/multifader2/23 > out.47 -touch./4/multifader2/24 > out.48 +touch./4/multifader1/{1..24} > out.{1..24} +touch./4/multifader2/{1..24} > out.{25..48} diff --git a/configs/osc-kbd.cfg b/configs/osc-kbd.cfg index eb80378..bd2e2c0 100644 --- a/configs/osc-kbd.cfg +++ b/configs/osc-kbd.cfg @@ -1,4 +1,5 @@ -; Maps a TouchOSC simpl keyboard layout to MIDI notes +; Maps a TouchOSC simple keyboard layout to MIDI notes +; and writes them out to a FLUIDSynth instance [backend midi] name = MIDIMonster @@ -11,15 +12,4 @@ bind = * 8000 dest = learn@8001 [map] -pad./1/push1 > out.ch0.note60 -pad./1/push2 > out.ch0.note61 -pad./1/push3 > out.ch0.note62 -pad./1/push4 > out.ch0.note63 -pad./1/push5 > out.ch0.note64 -pad./1/push6 > out.ch0.note65 -pad./1/push7 > out.ch0.note66 -pad./1/push8 > out.ch0.note67 -pad./1/push9 > out.ch0.note68 -pad./1/push10 > out.ch0.note69 -pad./1/push11 > out.ch0.note70 -pad./1/push12 > out.ch0.note71 +pad./1/push{1..12} > out.ch0.note{60..71} diff --git a/configs/unifest-17.cfg b/configs/unifest-17.cfg index 47e9ec2..2504550 100644 --- a/configs/unifest-17.cfg +++ b/configs/unifest-17.cfg @@ -1,27 +1,36 @@ -; Note that this configuration file was originally written with -; an older syntax and thus only contains right-to-left mappings +; This configuration was used as central control translator for the following tasks +; * Translate 2 Fader Wings and 2 Launch Control from MIDI CC to MIDI notes +; to be used as input to the GrandMA (connected to OUT A on Fader 1) +; Since both fader wings have the same name, we need to refer to them by portid +; -> Instances fader1, fader2, lc2, grandma +; * Remap buttons from a LaunchPad as input to the GrandMA +; -> Instances launchpad, grandma +; * Translate the rotaries of one Launch Control to ArtNet for additional effect control +; -> Instances lc1, xlaser +; +; Note that the MIDI port specifications might not be reusable 1:1 [backend midi] name = MIDIMonster [backend artnet] -bind = * 6454 +bind = 0.0.0.0 6454 net = 0 ; XLaser environment -[artnet claudius] -uni = 0 +[artnet xlaser] +universe = 0 ; MIDI input devices -[midi pad] +[midi launchpad] read = Launchpad write = Launchpad -[midi bcf1] +[midi fader1] read = 20:0 write = 20:0 -[midi bcf2] +[midi fader2] read = 36:0 write = 36:0 @@ -34,162 +43,32 @@ read = 32:0 write = 32:0 ; Output MIDI via OUT A on BCF -[midi out] +[midi grandma] write = 36:1 read = 36:1 [map] -; ArtNet -claudius.1 < lc1.ch0.cc1 -claudius.2 < lc1.ch0.cc2 -claudius.3 < lc1.ch0.cc3 -claudius.4 < lc1.ch0.cc4 -claudius.5 < lc1.ch0.cc5 -claudius.6 < lc1.ch0.cc6 -claudius.7 < lc1.ch0.cc7 -claudius.8 < lc1.ch0.cc8 -claudius.9 < lc1.ch0.cc9 -claudius.10 < lc1.ch0.cc10 -claudius.11 < lc1.ch0.cc11 -claudius.12 < lc1.ch0.cc12 -claudius.13 < lc1.ch0.cc13 -claudius.14 < lc1.ch0.cc14 -claudius.15 < lc1.ch0.cc15 -claudius.16 < lc1.ch0.cc16 +; Effect control +xlaser.{1..16} < lc1.ch0.cc{1..16} -; BCF Fader -out.ch0.ch0.note< bcf1.ch0.cc81 -out.ch0.note1 < bcf1.ch0.cc82 -out.ch0.note2 < bcf1.ch0.cc83 -out.ch0.note3 < bcf1.ch0.cc84 -out.ch0.note4 < bcf1.ch0.cc85 -out.ch0.note5 < bcf1.ch0.cc86 -out.ch0.note6 < bcf1.ch0.cc87 -out.ch0.note7 < bcf1.ch0.cc88 -out.ch0.note8 < bcf2.ch0.cc81 -out.ch0.note9 < bcf2.ch0.cc82 -out.ch0.note10 < bcf2.ch0.cc83 -out.ch0.note11 < bcf2.ch0.cc84 -out.ch0.note12 < bcf2.ch0.cc85 -out.ch0.note13 < bcf2.ch0.cc86 -out.ch0.note14 < bcf2.ch0.cc87 -out.ch0.note15 < bcf2.ch0.cc88 +; BCF Faders to GrandMA +grandma.ch0.note{0..7} < fader1.ch0.cc{81..88} +grandma.ch0.note{8..15} < fader2.ch0.cc{81..88} ; LC Rotary -out.ch0.note16 < lc1.ch0.cc1 -out.ch0.note17 < lc1.ch0.cc2 -out.ch0.note18 < lc1.ch0.cc3 -out.ch0.note19 < lc1.ch0.cc4 -out.ch0.note20 < lc1.ch0.cc5 -out.ch0.note21 < lc1.ch0.cc6 -out.ch0.note22 < lc1.ch0.cc7 -out.ch0.note23 < lc1.ch0.cc8 -out.ch0.note24 < lc1.ch0.cc9 -out.ch0.note25 < lc1.ch0.cc10 -out.ch0.note26 < lc1.ch0.cc11 -out.ch0.note27 < lc1.ch0.cc12 -out.ch0.note28 < lc1.ch0.cc13 -out.ch0.note29 < lc1.ch0.cc14 -out.ch0.note30 < lc1.ch0.cc15 -out.ch0.note31 < lc1.ch0.cc16 -out.ch0.note32 < lc2.ch0.cc1 -out.ch0.note33 < lc2.ch0.cc2 -out.ch0.note34 < lc2.ch0.cc3 -out.ch0.note35 < lc2.ch0.cc4 -out.ch0.note36 < lc2.ch0.cc5 -out.ch0.note37 < lc2.ch0.cc6 -out.ch0.note38 < lc2.ch0.cc7 -out.ch0.note39 < lc2.ch0.cc8 -out.ch0.note40 < lc2.ch0.cc9 -out.ch0.note41 < lc2.ch0.cc10 -out.ch0.note42 < lc2.ch0.cc11 -out.ch0.note43 < lc2.ch0.cc12 -out.ch0.note44 < lc2.ch0.cc13 -out.ch0.note45 < lc2.ch0.cc14 -out.ch0.note46 < lc2.ch0.cc15 -out.ch0.note47 < lc2.ch0.cc16 +grandma.ch0.note{16..31} < lc1.ch0.cc{1..16} +grandma.ch0.note{32..47} < lc2.ch0.cc{1..16} ; LC Button -out.ch0.note48 < lc1.ch0.note0 -out.ch0.note49 < lc1.ch0.note1 -out.ch0.note50 < lc1.ch0.note2 -out.ch0.note51 < lc1.ch0.note3 -out.ch0.note52 < lc1.ch0.note4 -out.ch0.note53 < lc1.ch0.note5 -out.ch0.note54 < lc1.ch0.note6 -out.ch0.note55 < lc1.ch0.note7 - -out.ch0.note56 < lc2.ch0.note0 -out.ch0.note57 < lc2.ch0.note1 -out.ch0.note58 < lc2.ch0.note2 -out.ch0.note59 < lc2.ch0.note3 -out.ch0.note60 < lc2.ch0.note4 -out.ch0.note61 < lc2.ch0.note5 -out.ch0.note62 < lc2.ch0.note6 -out.ch0.note63 < lc2.ch0.note7 +grandma.ch0.note{48..55} < lc1.ch0.note{0..7} +grandma.ch0.note{56..63} < lc2.ch0.note{0..7} ; Launchpad -out.ch0.note64 < pad.ch0.note0 -out.ch0.note65 < pad.ch0.note1 -out.ch0.note66 < pad.ch0.note2 -out.ch0.note67 < pad.ch0.note3 -out.ch0.note68 < pad.ch0.note4 -out.ch0.note69 < pad.ch0.note5 -out.ch0.note70 < pad.ch0.note6 -out.ch0.note71 < pad.ch0.note7 -out.ch0.note72 < pad.ch0.note16 -out.ch0.note73 < pad.ch0.note17 -out.ch0.note74 < pad.ch0.note18 -out.ch0.note75 < pad.ch0.note19 -out.ch0.note76 < pad.ch0.note20 -out.ch0.note77 < pad.ch0.note21 -out.ch0.note78 < pad.ch0.note22 -out.ch0.note79 < pad.ch0.note23 -out.ch0.note80 < pad.ch0.note32 -out.ch0.note81 < pad.ch0.note33 -out.ch0.note82 < pad.ch0.note34 -out.ch0.note83 < pad.ch0.note35 -out.ch0.note84 < pad.ch0.note36 -out.ch0.note85 < pad.ch0.note37 -out.ch0.note86 < pad.ch0.note38 -out.ch0.note87 < pad.ch0.note39 -out.ch0.note88 < pad.ch0.note48 -out.ch0.note89 < pad.ch0.note49 -out.ch0.note90 < pad.ch0.note50 -out.ch0.note91 < pad.ch0.note51 -out.ch0.note92 < pad.ch0.note52 -out.ch0.note93 < pad.ch0.note53 -out.ch0.note94 < pad.ch0.note54 -out.ch0.note95 < pad.ch0.note55 -out.ch0.note96 < pad.ch0.note64 -out.ch0.note97 < pad.ch0.note65 -out.ch0.note98 < pad.ch0.note66 -out.ch0.note99 < pad.ch0.note67 -out.ch0.note100 < pad.ch0.note68 -out.ch0.note101 < pad.ch0.note69 -out.ch0.note102 < pad.ch0.note70 -out.ch0.note103 < pad.ch0.note71 -out.ch0.note104 < pad.ch0.note80 -out.ch0.note105 < pad.ch0.note81 -out.ch0.note106 < pad.ch0.note82 -out.ch0.note107 < pad.ch0.note83 -out.ch0.note108 < pad.ch0.note84 -out.ch0.note109 < pad.ch0.note85 -out.ch0.note110 < pad.ch0.note86 -out.ch0.note111 < pad.ch0.note87 -out.ch0.note112 < pad.ch0.note96 -out.ch0.note113 < pad.ch0.note97 -out.ch0.note114 < pad.ch0.note98 -out.ch0.note115 < pad.ch0.note99 -out.ch0.note116 < pad.ch0.note100 -out.ch0.note117 < pad.ch0.note101 -out.ch0.note118 < pad.ch0.note102 -out.ch0.note119 < pad.ch0.note103 -out.ch0.note120 < pad.ch0.note112 -out.ch0.note121 < pad.ch0.note113 -out.ch0.note122 < pad.ch0.note114 -out.ch0.note123 < pad.ch0.note115 -out.ch0.note124 < pad.ch0.note116 -out.ch0.note125 < pad.ch0.note117 -out.ch0.note126 < pad.ch0.note118 -out.ch0.note127 < pad.ch0.note119 +grandma.ch0.note{64..71} < launchpad.ch0.note{0..7} +grandma.ch0.note{72..79} < launchpad.ch0.note{16..23} +grandma.ch0.note{80..87} < launchpad.ch0.note{32..39} +grandma.ch0.note{88..95} < launchpad.ch0.note{48..55} +grandma.ch0.note{96..103} < launchpad.ch0.note{64..71} +grandma.ch0.note{104..111} < launchpad.ch0.note{80..87} +grandma.ch0.note{112..119} < launchpad.ch0.note{96..103} +grandma.ch0.note{120..127} < launchpad.ch0.note{112..119} -- cgit v1.2.3 From c75721e77ecada3c88f4b493c1e3036c151bfe88 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 24 Jul 2019 21:33:23 +0200 Subject: Clarify backend documentation --- backends/evdev.md | 4 +++- backends/lua.c | 4 ++-- backends/lua.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backends/evdev.md b/backends/evdev.md index dfe5ec9..d750f1e 100644 --- a/backends/evdev.md +++ b/backends/evdev.md @@ -30,6 +30,8 @@ instances. The configuration value contains, space-separated, the following valu * `flat`: An offset, below which all deviations will be ignored * `resolution`: Axis resolution in units per millimeter (or units per radian for rotational axes) +If an axis is not used for output, this configuration can be omitted. + For real devices, all of these parameters for every axis can be found by running `evtest` on the device. #### Channel specification @@ -69,4 +71,4 @@ than `0`, respectively. As for output, only the values `-1`, `0` and `1` are gen `EV_KEY` key-down events are sent for normalized channel values over `0.9`. Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file -but may or may not work with the internal channel mapping and normalization code. \ No newline at end of file +but may or may not work with the internal channel mapping and normalization code. diff --git a/backends/lua.c b/backends/lua.c index 4d946cd..61e4e08 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -277,7 +277,7 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ lua_instance_data* data = (lua_instance_data*) inst->impl; //load a lua file into the interpreter - if(!strcmp(option, "script")){ + if(!strcmp(option, "script") || !strcmp(option, "source")){ if(luaL_dofile(data->interpreter, value)){ fprintf(stderr, "Failed to load lua source file %s for instance %s: %s\n", value, inst->name, lua_tostring(data->interpreter, -1)); return 1; @@ -285,7 +285,7 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ return 0; } - fprintf(stderr, "Unknown configuration parameter %s for lua backend\n", option); + fprintf(stderr, "Unknown configuration parameter %s for lua instance %s\n", option, inst->name); return 1; } diff --git a/backends/lua.md b/backends/lua.md index e273b28..1c67477 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -43,7 +43,7 @@ The backend does not take any global configuration. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `source` | `script.lua` | none | Lua source file | +| `script` | `script.lua` | none | Lua source file | A single instance may have multiple `source` options specified, which will all be read cumulatively. -- cgit v1.2.3 From 27eba094a067a0879250a3cf73c83df0da5c63a1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 24 Jul 2019 21:35:10 +0200 Subject: Add detect option to OSC backend --- backends/osc.c | 31 +++++++++++++++++++++++++------ backends/osc.md | 6 ++++-- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/backends/osc.c b/backends/osc.c index 1305169..36b0993 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -13,6 +13,12 @@ #define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4) #define BACKEND_NAME "osc" +static struct { + uint8_t detect; +} osc_global_config = { + .detect = 0 +}; + int init(){ backend osc = { .name = BACKEND_NAME, @@ -248,7 +254,15 @@ static int osc_validate_path(char* path){ } static int osc_configure(char* option, char* value){ - fprintf(stderr, "The OSC backend does not take any global configuration\n"); + if(!strcmp(option, "detect")){ + osc_global_config.detect = 1; + if(!strcmp(value, "off")){ + osc_global_config.detect = 0; + } + return 0; + } + + fprintf(stderr, "Unknown configuration parameter %s for OSC backend\n", option); return 1; } @@ -384,7 +398,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ return 0; } - fprintf(stderr, "Unknown configuration parameter %s for OSC backend\n", option); + fprintf(stderr, "Unknown configuration parameter %s for OSC instance %s\n", option, inst->name); return 1; } @@ -585,16 +599,17 @@ static int osc_handle(size_t num, managed_fd* fds){ else{ bytes_read = recv(fds[fd].fd, recv_buf, sizeof(recv_buf), 0); } + + if(bytes_read < 0){ + break; + } + if(data->root && strncmp(recv_buf, data->root, min(bytes_read, strlen(data->root)))){ //ignore packet for different root continue; } osc_local = recv_buf + (data->root ? strlen(data->root) : 0); - if(bytes_read < 0){ - break; - } - osc_fmt = recv_buf + osc_align(strlen(recv_buf) + 1); if(*osc_fmt != ','){ //invalid format string @@ -603,6 +618,10 @@ static int osc_handle(size_t num, managed_fd* fds){ } osc_fmt++; + if(osc_global_config.detect){ + fprintf(stderr, "Incoming OSC data: Path %s.%s Format %s\n", inst->name, osc_local, osc_fmt); + } + osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); //FIXME check supplied data length diff --git a/backends/osc.md b/backends/osc.md index c784cda..e9aa4d5 100644 --- a/backends/osc.md +++ b/backends/osc.md @@ -5,7 +5,9 @@ spoken primarily by visual interface tools and hardware such as TouchOSC. #### Global configuration -This backend does not take any global configuration. +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `detect` | `on` | `off` | Output the path of all incoming OSC packets to allow for easier configuration. Any path filters configured using the `root` instance configuration options still apply. | #### Instance configuration @@ -78,4 +80,4 @@ The default ranges are: #### Known bugs / problems -Ping requests are not yet answered. There may be some problems using broadcast output and input. \ No newline at end of file +Ping requests are not yet answered. There may be some problems using broadcast output and input. -- cgit v1.2.3 From 8d7fb5b7cb2f1deb6600f8cebfff27dba70193b1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 24 Jul 2019 21:38:45 +0200 Subject: Allow n:1 multi-channel mapping --- README.md | 3 ++- config.c | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bce7b71..3704f5f 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,8 @@ delimiting a range of channels. Multiple such expressions may be used in one ch specification, with the rightmost expression being incremented (or decremented) first for evaluation. -Both sides of a multi-channel assignment need to have the same number of channels. +Both sides of a multi-channel assignment need to have the same number of channels, or one +side must have exactly one channel. Example multi-channel mapping: diff --git a/config.c b/config.c index 24f1223..6d5fd16 100644 --- a/config.c +++ b/config.c @@ -220,7 +220,7 @@ static int config_map(char* to_raw, char* from_raw){ goto done; } - if(spec_to.channels != spec_from.channels + if((spec_to.channels != spec_from.channels && spec_from.channels != 1 && spec_to.channels != 1) || spec_to.channels == 0 || spec_from.channels == 0){ fprintf(stderr, "Multi-channel specification size mismatch: %s.%s (%lu channels) - %s.%s (%lu channels)\n", @@ -235,9 +235,9 @@ static int config_map(char* to_raw, char* from_raw){ //iterate, resolve globs and map rv = 0; - for(n = 0; !rv && n < spec_from.channels; n++){ - channel_from = config_glob_resolve(instance_from, &spec_from, n); - channel_to = config_glob_resolve(instance_to, &spec_to, n); + for(n = 0; !rv && n < max(spec_from.channels, spec_to.channels); n++){ + channel_from = config_glob_resolve(instance_from, &spec_from, min(n, spec_from.channels)); + channel_to = config_glob_resolve(instance_to, &spec_to, min(n, spec_to.channels)); if(!channel_from || !channel_to){ rv = 1; -- cgit v1.2.3 From 26ee2eacc7d60aa379c9e4b9b9c6b8bcdcd4bc6b Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 27 Jul 2019 19:31:21 +0200 Subject: Refactor OSC backend, implement pattern matching --- README.md | 4 +- TODO | 1 - backends/Makefile | 4 +- backends/osc.c | 635 ++++++++++++++++++++++++++++++++-------------- backends/osc.h | 25 +- backends/osc.md | 21 +- configs/flying-faders.cfg | 24 ++ configs/flying-faders.lua | 10 + configs/osc-xy.cfg | 26 ++ 9 files changed, 543 insertions(+), 207 deletions(-) create mode 100644 configs/flying-faders.cfg create mode 100644 configs/flying-faders.lua create mode 100644 configs/osc-xy.cfg diff --git a/README.md b/README.md index 3704f5f..9647d4f 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ Currently, the MIDIMonster supports the following protocols: * MIDI (Linux, via ALSA) * ArtNet -* sACN / E1.31 -* OSC +* Streaming ACN (sACN / E1.31) +* OpenSoundControl (OSC) * evdev input devices (Linux) * Open Lighting Architecture (OLA) diff --git a/TODO b/TODO index d114640..2a97c30 100644 --- a/TODO +++ b/TODO @@ -3,6 +3,5 @@ Note source in channel value struct Optimize core channel search (store backend offset) Printing backend / Verbose mode -document example configs evdev relative axis size mm_managed_fd.impl is not freed currently diff --git a/backends/Makefile b/backends/Makefile index fe88669..22cb95b 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -6,8 +6,8 @@ BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) -CFLAGS += -fPIC -I../ -CPPFLAGS += -fPIC -I../ +CFLAGS += -g -fPIC -I../ +CPPFLAGS += -g -fPIC -I../ LDFLAGS += -shared # Build Linux backends if possible diff --git a/backends/osc.c b/backends/osc.c index 36b0993..3f19abf 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -13,6 +13,14 @@ #define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4) #define BACKEND_NAME "osc" +typedef union { + struct { + uint32_t channel; + uint32_t parameter; + } fields; + uint64_t label; +} osc_channel_ident; + static struct { uint8_t detect; } osc_global_config = { @@ -41,6 +49,7 @@ int init(){ } static size_t osc_data_length(osc_parameter_type t){ + //binary representation lengths for osc data types switch(t){ case int32: case float32: @@ -55,6 +64,7 @@ static size_t osc_data_length(osc_parameter_type t){ } static inline void osc_defaults(osc_parameter_type t, osc_parameter_value* max, osc_parameter_value* min){ + //data type default ranges memset(max, 0, sizeof(osc_parameter_value)); memset(min, 0, sizeof(osc_parameter_value)); switch(t){ @@ -77,6 +87,7 @@ static inline void osc_defaults(osc_parameter_type t, osc_parameter_value* max, } static inline osc_parameter_value osc_parse(osc_parameter_type t, uint8_t* data){ + //read value from binary representation osc_parameter_value v = {0}; switch(t){ case int32: @@ -94,6 +105,7 @@ static inline osc_parameter_value osc_parse(osc_parameter_type t, uint8_t* data) } static inline int osc_deparse(osc_parameter_type t, osc_parameter_value v, uint8_t* data){ + //write value to binary representation uint64_t u64 = 0; uint32_t u32 = 0; switch(t){ @@ -115,6 +127,7 @@ static inline int osc_deparse(osc_parameter_type t, osc_parameter_value v, uint8 } static inline osc_parameter_value osc_parse_value_spec(osc_parameter_type t, char* value){ + //read value from string osc_parameter_value v = {0}; switch(t){ case int32: @@ -136,6 +149,7 @@ static inline osc_parameter_value osc_parse_value_spec(osc_parameter_type t, cha } static inline channel_value osc_parameter_normalise(osc_parameter_type t, osc_parameter_value min, osc_parameter_value max, osc_parameter_value cur){ + //normalise osc value wrt given min/max channel_value v = { .raw = {0}, .normalised = 0 @@ -179,6 +193,7 @@ static inline channel_value osc_parameter_normalise(osc_parameter_type t, osc_pa } static inline osc_parameter_value osc_parameter_denormalise(osc_parameter_type t, osc_parameter_value min, osc_parameter_value max, channel_value cur){ + //convert normalised value to osc value wrt given min/max osc_parameter_value v = {0}; union { @@ -212,45 +227,192 @@ static inline osc_parameter_value osc_parameter_denormalise(osc_parameter_type t return v; } -static int osc_generate_event(channel* c, osc_channel* info, char* fmt, uint8_t* data, size_t data_len){ - size_t p, off = 0; - if(!c || !info){ - return 0; +static int osc_path_validate(char* path, uint8_t allow_patterns){ + //validate osc path or pattern + char illegal_chars[] = " #,"; + char pattern_chars[] = "?[]{}*"; + size_t u, c; + uint8_t square_open = 0, curly_open = 0; + + if(path[0] != '/'){ + fprintf(stderr, "%s is not a valid OSC path: Missing root /\n", path); + return 1; } - osc_parameter_value min, max, cur; - channel_value evt; + for(u = 0; u < strlen(path); u++){ + for(c = 0; c < sizeof(illegal_chars); c++){ + if(path[u] == illegal_chars[c]){ + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, illegal_chars[c], u); + return 1; + } + } - if(!fmt || !data || data_len % 4 || !*fmt){ - fprintf(stderr, "Invalid OSC packet, data length %zu\n", data_len); - return 1; - } + if(!isgraph(path[u])){ + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, pattern_chars[c], u); + return 1; + } - //find offset for this parameter - for(p = 0; p < info->param_index; p++){ - off += osc_data_length(fmt[p]); - } + if(!allow_patterns){ + for(c = 0; c < sizeof(pattern_chars); c++){ + if(path[u] == pattern_chars[c]){ + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, pattern_chars[c], u); + return 1; + } + } + } - if(info->type != not_set){ - max = info->max; - min = info->min; + switch(path[u]){ + case '{': + if(square_open || curly_open){ + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, pattern_chars[c], u); + return 1; + } + curly_open = 1; + break; + case '[': + if(square_open || curly_open){ + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, pattern_chars[c], u); + return 1; + } + square_open = 1; + break; + case '}': + curly_open = 0; + break; + case ']': + square_open = 0; + break; + case '/': + if(square_open || curly_open){ + fprintf(stderr, "%s is not a valid OSC path: Pattern across part boundaries\n", path); + return 1; + } + } } - else{ - osc_defaults(fmt[info->param_index], &max, &min); + + if(square_open || curly_open){ + fprintf(stderr, "%s is not a valid OSC path: Unterminated pattern expression\n", path); + return 1; } + return 0; +} - cur = osc_parse(fmt[info->param_index], data + off); - evt = osc_parameter_normalise(fmt[info->param_index], min, max, cur); +static int osc_path_match(char* pattern, char* path){ + size_t u, p = 0, match_begin, match_end; + uint8_t match_any = 0, inverted, match; - return mm_channel_event(c, evt); -} + for(u = 0; u < strlen(path); u++){ + switch(pattern[p]){ + case '/': + if(match_any){ + for(; path[u] && path[u] != '/'; u++){ + } + } + if(path[u] != '/'){ + return 0; + } + match_any = 0; + p++; + break; + case '?': + match_any = 0; + p++; + break; + case '*': + match_any = 1; + p++; + break; + case '[': + inverted = (pattern[p + 1] == '!') ? 1 : 0; + match_end = match_begin = inverted ? p + 2 : p + 1; + match = 0; + for(; pattern[match_end] != ']'; match_end++){ + if(pattern[match_end] == path[u]){ + match = 1; + break; + } -static int osc_validate_path(char* path){ - if(path[0] != '/'){ - fprintf(stderr, "%s is not a valid OSC path: Missing root /\n", path); - return 1; + if(pattern[match_end + 1] == '-' && pattern[match_end + 2] != ']'){ + if((pattern[match_end] > pattern[match_end + 2] + && path[u] >= pattern[match_end + 2] + && path[u] <= pattern[match_end]) + || (pattern[match_end] <= pattern[match_end + 2] + && path[u] >= pattern[match_end] + && path[u] <= pattern[match_end + 2])){ + match = 1; + break; + } + match_end += 2; + } + + if(pattern[match_end + 1] == ']' && match_any && !match + && path[u + 1] && path[u + 1] != '/'){ + match_end = match_begin - 1; + u++; + } + } + + if(match == inverted){ + return 0; + } + + match_any = 0; + //advance to end of pattern + for(; pattern[p] != ']'; p++){ + } + p++; + break; + case '{': + for(match_begin = p + 1; pattern[match_begin] != '}'; match_begin++){ + //find end + for(match_end = match_begin; pattern[match_end] != ',' && pattern[match_end] != '}'; match_end++){ + } + + if(!strncmp(path + u, pattern + match_begin, match_end - match_begin)){ + //advance pattern + for(; pattern[p] != '}'; p++){ + } + p++; + //advance path + u += match_end - match_begin - 1; + break; + } + + if(pattern[match_end] == '}'){ + //retry with next if in match_any + if(match_any && path[u + 1] && path[u + 1] != '/'){ + u++; + match_begin = p; + continue; + } + return 0; + } + match_begin = match_end; + } + match_any = 0; + break; + case 0: + if(match_any){ + for(; path[u] && path[u] != '/'; u++){ + } + } + if(path[u]){ + return 0; + } + break; + default: + if(match_any){ + for(; path[u] && path[u] != '/' && path[u] != pattern[p]; u++){ + } + } + if(pattern[p] != path[u]){ + return 0; + } + p++; + break; + } } - return 0; + return 1; } static int osc_configure(char* option, char* value){ @@ -266,13 +428,81 @@ static int osc_configure(char* option, char* value){ return 1; } +static int osc_register_pattern(osc_instance_data* data, char* pattern_path, char* configuration){ + size_t u, pattern; + char* format = NULL, *token = NULL; + + if(osc_path_validate(pattern_path, 1)){ + fprintf(stderr, "Not a valid OSC pattern: %s\n", pattern_path); + return 1; + } + + //tokenize configuration + format = strtok(configuration, " "); + if(!format || strlen(format) < 1){ + fprintf(stderr, "Not a valid format specification for OSC pattern %s\n", pattern_path); + return 1; + } + + //create pattern + data->pattern = realloc(data->pattern, (data->patterns + 1) * sizeof(osc_channel)); + if(!data->pattern){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + pattern = data->patterns; + + data->pattern[pattern].params = strlen(format); + data->pattern[pattern].path = strdup(pattern_path); + data->pattern[pattern].type = calloc(strlen(format), sizeof(osc_parameter_type)); + data->pattern[pattern].max = calloc(strlen(format), sizeof(osc_parameter_value)); + data->pattern[pattern].min = calloc(strlen(format), sizeof(osc_parameter_value)); + + if(!data->pattern[pattern].path + || !data->pattern[pattern].type + || !data->pattern[pattern].max + || !data->pattern[pattern].min){ + //this should fail config parsing and thus call the shutdown function, + //which should properly free the rest of the data + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + //check format validity and store min/max values + for(u = 0; u < strlen(format); u++){ + if(!osc_data_length(format[u])){ + fprintf(stderr, "Invalid format specifier %c for pattern %s\n", format[u], pattern_path); + return 1; + } + + data->pattern[pattern].type[u] = format[u]; + + //parse min/max values + token = strtok(NULL, " "); + if(!token){ + fprintf(stderr, "Missing minimum specification for parameter %zu of OSC pattern %s\n", u, pattern_path); + return 1; + } + data->pattern[pattern].min[u] = osc_parse_value_spec(format[u], token); + + token = strtok(NULL, " "); + if(!token){ + fprintf(stderr, "Missing maximum specification for parameter %zu of OSC pattern %s\n", u, pattern_path); + return 1; + } + data->pattern[pattern].max[u] = osc_parse_value_spec(format[u], token); + } + + data->patterns++; + return 0; +} + static int osc_configure_instance(instance* inst, char* option, char* value){ osc_instance_data* data = (osc_instance_data*) inst->impl; - char* host = NULL, *port = NULL, *token = NULL, *format = NULL; - size_t u, p; + char* host = NULL, *port = NULL, *token = NULL; if(!strcmp(option, "root")){ - if(osc_validate_path(value)){ + if(osc_path_validate(value, 0)){ fprintf(stderr, "Not a valid OSC root: %s\n", value); return 1; } @@ -326,76 +556,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ return 0; } else if(*option == '/'){ - //pre-configure channel - if(osc_validate_path(option)){ - fprintf(stderr, "Not a valid OSC path: %s\n", option); - return 1; - } - - for(u = 0; u < data->channels; u++){ - if(!strcmp(option, data->channel[u].path)){ - fprintf(stderr, "OSC channel %s already configured\n", option); - return 1; - } - } - - //tokenize configuration - format = strtok(value, " "); - if(!format || strlen(format) < 1){ - fprintf(stderr, "Not a valid format for OSC path %s\n", option); - return 1; - } - - //check format validity, create subchannels - for(p = 0; p < strlen(format); p++){ - if(!osc_data_length(format[p])){ - fprintf(stderr, "Invalid format specifier %c for path %s, ignoring\n", format[p], option); - continue; - } - - //register new sub-channel - data->channel = realloc(data->channel, (data->channels + 1) * sizeof(osc_channel)); - if(!data->channel){ - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - - memset(data->channel + data->channels, 0, sizeof(osc_channel)); - data->channel[data->channels].params = strlen(format); - data->channel[data->channels].param_index = p; - data->channel[data->channels].type = format[p]; - data->channel[data->channels].path = strdup(option); - - if(!data->channel[data->channels].path){ - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - - //parse min/max values - token = strtok(NULL, " "); - if(!token){ - fprintf(stderr, "Missing minimum specification for parameter %zu of %s\n", p, option); - return 1; - } - data->channel[data->channels].min = osc_parse_value_spec(format[p], token); - - token = strtok(NULL, " "); - if(!token){ - fprintf(stderr, "Missing maximum specification for parameter %zu of %s\n", p, option); - return 1; - } - data->channel[data->channels].max = osc_parse_value_spec(format[p], token); - - //allocate channel from core - if(!mm_channel(inst, data->channels, 1)){ - fprintf(stderr, "Failed to register core channel\n"); - return 1; - } - - //increase channel count - data->channels++; - } - return 0; + return osc_register_pattern(data, option, value); } fprintf(stderr, "Unknown configuration parameter %s for OSC instance %s\n", option, inst->name); @@ -420,31 +581,38 @@ static instance* osc_instance(){ } static channel* osc_map_channel(instance* inst, char* spec){ - size_t u; + size_t u, p; osc_instance_data* data = (osc_instance_data*) inst->impl; - size_t param_index = 0; + osc_channel_ident ident = { + .label = 0 + }; //check spec for correctness - if(osc_validate_path(spec)){ + if(osc_path_validate(spec, 0)){ return NULL; } //parse parameter offset if(strrchr(spec, ':')){ - param_index = strtoul(strrchr(spec, ':') + 1, NULL, 10); + ident.fields.parameter = strtoul(strrchr(spec, ':') + 1, NULL, 10); *(strrchr(spec, ':')) = 0; } //find matching channel for(u = 0; u < data->channels; u++){ - if(!strcmp(spec, data->channel[u].path) && data->channel[u].param_index == param_index){ - //fprintf(stderr, "Reusing previously created channel %s parameter %zu\n", data->channel[u].path, data->channel[u].param_index); + if(!strcmp(spec, data->channel[u].path)){ break; } } //allocate new channel if(u == data->channels){ + for(p = 0; p < data->patterns; p++){ + if(osc_path_match(data->pattern[p].path, spec)){ + break; + } + } + data->channel = realloc(data->channel, (u + 1) * sizeof(osc_channel)); if(!data->channel){ fprintf(stderr, "Failed to allocate memory\n"); @@ -452,22 +620,100 @@ static channel* osc_map_channel(instance* inst, char* spec){ } memset(data->channel + u, 0, sizeof(osc_channel)); - data->channel[u].param_index = param_index; data->channel[u].path = strdup(spec); + if(p != data->patterns){ + fprintf(stderr, "Matched pattern %s for %s\n", data->pattern[p].path, spec); + data->channel[u].params = data->pattern[p].params; + //just reuse the pointers from the pattern + data->channel[u].type = data->pattern[p].type; + data->channel[u].max = data->pattern[p].max; + data->channel[u].min = data->pattern[p].min; + + //these are per channel + data->channel[u].in = calloc(data->channel[u].params, sizeof(osc_parameter_value)); + data->channel[u].out = calloc(data->channel[u].params, sizeof(osc_parameter_value)); + } + else if(data->patterns){ + fprintf(stderr, "No pattern match found for %s\n", spec); + } - if(!data->channel[u].path){ + if(!data->channel[u].path + || (data->channel[u].params && (!data->channel[u].in || !data->channel[u].out))){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; } data->channels++; } - return mm_channel(inst, u, 1); + ident.fields.channel = u; + return mm_channel(inst, ident.label, 1); +} + +static int osc_output_channel(instance* inst, size_t channel){ + osc_instance_data* data = (osc_instance_data*) inst->impl; + uint8_t xmit_buf[OSC_XMIT_BUF] = "", *format = NULL; + size_t offset = 0, p; + + //fix destination rport if required + if(data->forced_rport){ + //cheating a bit because both IPv4 and IPv6 have the port at the same offset + struct sockaddr_in* sockadd = (struct sockaddr_in*) &(data->dest); + sockadd->sin_port = htobe16(data->forced_rport); + } + + //determine minimum packet size + if(osc_align((data->root ? strlen(data->root) : 0) + strlen(data->channel[channel].path) + 1) + osc_align(data->channel[channel].params + 2) >= sizeof(xmit_buf)){ + fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s\n", inst->name, data->channel[channel].path); + return 1; + } + + //copy osc target path + if(data->root){ + memcpy(xmit_buf, data->root, strlen(data->root)); + offset += strlen(data->root); + } + + memcpy(xmit_buf + offset, data->channel[channel].path, strlen(data->channel[channel].path)); + offset += strlen(data->channel[channel].path) + 1; + offset = osc_align(offset); + + //get format string offset, initialize + format = xmit_buf + offset; + offset += osc_align(data->channel[channel].params + 2); + *format = ','; + format++; + + for(p = 0; p < data->channel[channel].params; p++){ + //write format specifier + format[p] = data->channel[channel].type[p]; + + //write data + if(offset + osc_data_length(data->channel[channel].type[p]) >= sizeof(xmit_buf)){ + fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %zu\n", inst->name, data->channel[channel].path, p); + return 1; + } + + osc_deparse(data->channel[channel].type[p], + data->channel[channel].out[p], + xmit_buf + offset); + offset += osc_data_length(data->channel[channel].type[p]); + } + + //output packet + if(sendto(data->fd, xmit_buf, offset, 0, (struct sockaddr*) &(data->dest), data->dest_len) < 0){ + fprintf(stderr, "Failed to transmit OSC packet: %s\n", strerror(errno)); + } + return 0; } static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ - uint8_t xmit_buf[OSC_XMIT_BUF], *format = NULL; - size_t evt = 0, off, members, p; + size_t evt = 0, mark = 0; + int rv; + osc_channel_ident ident = { + .label = 0 + }; + osc_parameter_value current; + if(!num){ return 0; } @@ -479,95 +725,97 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ } for(evt = 0; evt < num; evt++){ - off = c[evt]->ident; + ident.label = c[evt]->ident; //sanity check - if(off >= data->channels){ + if(ident.fields.channel >= data->channels + || ident.fields.parameter >= data->channel[ident.fields.channel].params){ fprintf(stderr, "OSC channel identifier out of range\n"); return 1; } //if the format is unknown, don't output - if(data->channel[off].type == not_set || data->channel[off].params == 0){ - fprintf(stderr, "OSC channel %s.%s requires format specification for output\n", inst->name, data->channel[off].path); + if(!data->channel[ident.fields.channel].params){ + fprintf(stderr, "OSC channel %s.%s requires format specification for output\n", inst->name, data->channel[ident.fields.channel].path); continue; } - //update current value - data->channel[off].current = osc_parameter_denormalise(data->channel[off].type, data->channel[off].min, data->channel[off].max, v[evt]); - //mark channel - data->channel[off].mark = 1; + //only output on change + current = osc_parameter_denormalise(data->channel[ident.fields.channel].type[ident.fields.parameter], + data->channel[ident.fields.channel].min[ident.fields.parameter], + data->channel[ident.fields.channel].max[ident.fields.parameter], + v[evt]); + if(memcmp(¤t, &data->channel[ident.fields.channel].out[ident.fields.parameter], sizeof(current))){ + //update current value + data->channel[ident.fields.channel].out[ident.fields.parameter] = current; + //mark channel + data->channel[ident.fields.channel].mark = 1; + mark = 1; + } } - - //fix destination rport if required - if(data->forced_rport){ - //cheating a bit because both IPv4 and IPv6 have the port at the same offset - struct sockaddr_in* sockadd = (struct sockaddr_in*) &(data->dest); - sockadd->sin_port = htobe16(data->forced_rport); + + if(mark){ + //output all marked channels + for(evt = 0; !rv && evt < num; evt++){ + ident.label = c[evt]->ident; + if(data->channel[ident.fields.channel].mark){ + rv |= osc_output_channel(inst, ident.fields.channel); + data->channel[ident.fields.channel].mark = 0; + } + } } + return rv; +} - //find all marked channels - for(evt = 0; evt < data->channels; evt++){ - //zero output buffer - memset(xmit_buf, 0, sizeof(xmit_buf)); - if(data->channel[evt].mark){ - //determine minimum packet size - if(osc_align((data->root ? strlen(data->root) : 0) + strlen(data->channel[evt].path) + 1) + osc_align(data->channel[evt].params + 2) >= sizeof(xmit_buf)){ - fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s\n", inst->name, data->channel[evt].path); - return 1; - } +static int osc_process_packet(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){ + osc_instance_data* data = (osc_instance_data*) inst->impl; + size_t c, p, offset = 0; + osc_parameter_value min, max, cur; + channel_value evt; + osc_channel_ident ident = { + .label = 0 + }; + channel* chan = NULL; - off = 0; - //copy osc target path - if(data->root){ - memcpy(xmit_buf, data->root, strlen(data->root)); - off += strlen(data->root); - } - memcpy(xmit_buf + off, data->channel[evt].path, strlen(data->channel[evt].path)); - off += strlen(data->channel[evt].path) + 1; - off = osc_align(off); - - //get format string offset, initialize - format = xmit_buf + off; - off += osc_align(data->channel[evt].params + 2); - *format = ','; - format++; - - //gather subchannels, unmark - members = 0; - for(p = 0; p < data->channels && members < data->channel[evt].params; p++){ - if(!strcmp(data->channel[evt].path, data->channel[p].path)){ - //unmark channel - data->channel[p].mark = 0; - - //sanity check - if(data->channel[p].param_index >= data->channel[evt].params){ - fprintf(stderr, "OSC channel %s.%s has multiple parameter offset definitions\n", inst->name, data->channel[evt].path); - return 1; - } + if(payload_len % 4){ + fprintf(stderr, "Invalid OSC packet, data length %zu\n", payload_len); + return 0; + } - //write format specifier - format[data->channel[p].param_index] = data->channel[p].type; + for(c = 0; c < data->channels; c++){ + if(!strcmp(local_path, data->channel[c].path)){ + ident.fields.channel = c; + //unconfigured input should work without errors (using default limits) + if(data->channel[c].params && strlen(format) != data->channel[c].params){ + fprintf(stderr, "OSC message %s.%s had format %s, internal representation has %lu parameters\n", inst->name, local_path, format, data->channel[c].params); + continue; + } - //write data - //FIXME this currently depends on all channels being registered in the correct order, since it just appends data - if(off + osc_data_length(data->channel[p].type) >= sizeof(xmit_buf)){ - fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %zu\n", inst->name, data->channel[evt].path, members); - return 1; + for(p = 0; p < strlen(format); p++){ + ident.fields.parameter = p; + if(data->channel[c].params){ + max = data->channel[c].max[p]; + min = data->channel[c].min[p]; + } + else{ + osc_defaults(format[p], &max, &min); + } + cur = osc_parse(format[p], payload + offset); + if(!data->channel[c].params || memcmp(&cur, &data->channel[c].in, sizeof(cur))){ + evt = osc_parameter_normalise(format[p], min, max, cur); + chan = mm_channel(inst, ident.label, 0); + if(chan){ + mm_channel_event(chan, evt); } - - osc_deparse(data->channel[p].type, data->channel[p].current, xmit_buf + off); - off += osc_data_length(data->channel[p].type); - members++; } - } - //output packet - if(sendto(data->fd, xmit_buf, off, 0, (struct sockaddr*) &(data->dest), data->dest_len) < 0){ - fprintf(stderr, "Failed to transmit OSC packet: %s\n", strerror(errno)); + //skip to next parameter data + offset += osc_data_length(format[p]); + //TODO check offset against payload length } } } + return 0; } @@ -577,7 +825,6 @@ static int osc_handle(size_t num, managed_fd* fds){ instance* inst = NULL; osc_instance_data* data = NULL; ssize_t bytes_read = 0; - size_t c; char* osc_fmt = NULL; char* osc_local = NULL; uint8_t* osc_data = NULL; @@ -600,7 +847,7 @@ static int osc_handle(size_t num, managed_fd* fds){ bytes_read = recv(fds[fd].fd, recv_buf, sizeof(recv_buf), 0); } - if(bytes_read < 0){ + if(bytes_read <= 0){ break; } @@ -622,20 +869,11 @@ static int osc_handle(size_t num, managed_fd* fds){ fprintf(stderr, "Incoming OSC data: Path %s.%s Format %s\n", inst->name, osc_local, osc_fmt); } - osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); //FIXME check supplied data length + osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); - for(c = 0; c < data->channels; c++){ - //FIXME implement proper OSC path match - //prefix match - if(!strcmp(osc_local, data->channel[c].path)){ - if(strlen(osc_fmt) > data->channel[c].param_index){ - //fprintf(stderr, "Taking parameter %zu of %s (%s), %zd bytes, data offset %zu\n", data->channel[c].param_index, recv_buf, osc_fmt, bytes_read, (osc_data - (uint8_t*)recv_buf)); - if(osc_generate_event(mm_channel(inst, c, 0), data->channel + c, osc_fmt, osc_data, bytes_read - (osc_data - (uint8_t*) recv_buf))){ - fprintf(stderr, "Failed to generate OSC channel event\n"); - } - } - } + if(osc_process_packet(inst, osc_local, osc_fmt, osc_data, bytes_read - (osc_data - (uint8_t*) recv_buf))){ + return 1; } } while(bytes_read > 0); @@ -706,14 +944,25 @@ static int osc_shutdown(){ data = (osc_instance_data*) inst[u]->impl; for(c = 0; c < data->channels; c++){ free(data->channel[c].path); + free(data->channel[c].in); + free(data->channel[c].out); } free(data->channel); + for(c = 0; c < data->patterns; c++){ + free(data->pattern[c].path); + free(data->pattern[c].type); + free(data->pattern[c].min); + free(data->pattern[c].max); + } + free(data->pattern); + free(data->root); if(data->fd >= 0){ close(data->fd); } data->fd = -1; data->channels = 0; + data->patterns = 0; free(inst[u]->impl); } diff --git a/backends/osc.h b/backends/osc.h index dc6cb3a..4e9dec5 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -34,22 +34,33 @@ typedef union { typedef struct /*_osc_channel*/ { char* path; size_t params; - size_t param_index; uint8_t mark; - osc_parameter_type type; - osc_parameter_value max; - osc_parameter_value min; - osc_parameter_value current; + osc_parameter_type* type; + osc_parameter_value* max; + osc_parameter_value* min; + osc_parameter_value* in; + osc_parameter_value* out; } osc_channel; typedef struct /*_osc_instance_data*/ { + //pre-configured channel patterns + size_t patterns; + osc_channel* pattern; + + //actual channel registry size_t channels; osc_channel* channel; + + //instance config char* root; + uint8_t learn; + + //peer addressing socklen_t dest_len; struct sockaddr_storage dest; - int fd; - uint8_t learn; uint16_t forced_rport; + + //peer fd + int fd; } osc_instance_data; diff --git a/backends/osc.md b/backends/osc.md index e9aa4d5..b7ce527 100644 --- a/backends/osc.md +++ b/backends/osc.md @@ -23,13 +23,21 @@ it are ignored early in processing. Channels that are to be output or require a value range different from the default ranges (see below) require special configuration, as their types and limits have to be set. -This is done in the instance configuration using an assignment of the syntax +This is done by specifying *patterns* in the instance configuration using an assignment of the syntax ``` /local/osc/path = ... ``` -The OSC path to be configured must only be the local part (omitting a configured instance root). +The pattern will be matched only against the local part (that is, the path excluding any configured instance root). +Patterns may contain the following expressions (conforming to the [OSC pattern matching specification](http://opensoundcontrol.org/spec-1_0)): +* `?` matches any single legal character +* `*` matches zero or more legal characters +* A comma-separated list of strings inside curly braces `{}` matches any of the strings +* A string of characters within square brackets `[]` matches any character in the string + * Two characters with a `-` between them specify a range of characters + * An exclamation mark immediately after the opening `[` negates the meaning of the expression (ie. it matches characters not in the range) +* Any other legal character matches only itself **format** may be any sequence of valid OSC type characters. See below for a table of supported OSC types. @@ -44,6 +52,12 @@ a range between 0.0 and 2.0 (for example, an X-Y control), would look as follows /1/xy1 = ff 0.0 2.0 0.0 2.0 ``` +To configure a range of faders, an expression similar to the following line could be used + +``` +/1/fader* = f 0.0 1.0 +``` + #### Channel specification A channel may be any valid OSC path, to which the instance root will be prepended if @@ -80,4 +94,7 @@ The default ranges are: #### Known bugs / problems +The OSC path match currently works on the unit of characters. This may lead to some unexpected results +when matching expressions of the form `*`. + Ping requests are not yet answered. There may be some problems using broadcast output and input. diff --git a/configs/flying-faders.cfg b/configs/flying-faders.cfg new file mode 100644 index 0000000..4197581 --- /dev/null +++ b/configs/flying-faders.cfg @@ -0,0 +1,24 @@ +; Create a 'flying faders' effect using lua and output +; it onto TouchOSC (Layout 'Mix16', Multifader view on page 4) + +[osc touch] +bind = * 8000 +dest = learn@9000 + +; Pre-declare the fader values so the range mapping is correct +/*/fader* = f 0.0 1.0 +/*/toggle* = f 0.0 1.0 +/*/push* = f 0.0 1.0 +/*/multifader*/* = f 0.0 1.0 +/1/xy = ff 0.0 1.0 0.0 1.0 + +[lua generator] +script = configs/flying-faders.lua + +[map] + +generator.wave{1..24} > touch./4/multifader1/{1..24} +;generator.wave{1..24} > touch./4/multifader2/{1..24} + +touch./4/multifader2/1 > generator.magnitude + diff --git a/configs/flying-faders.lua b/configs/flying-faders.lua new file mode 100644 index 0000000..0b0faef --- /dev/null +++ b/configs/flying-faders.lua @@ -0,0 +1,10 @@ +step = 0 + +function wave() + for chan=1,24 do + output("wave" .. chan, (math.sin(math.rad((step + chan * 360 / 24) % 360)) + 1) / 2) + end + step = (step + 5) % 360 +end + +interval(wave, 100) diff --git a/configs/osc-xy.cfg b/configs/osc-xy.cfg new file mode 100644 index 0000000..fc5c5f3 --- /dev/null +++ b/configs/osc-xy.cfg @@ -0,0 +1,26 @@ +; Test for bi-directional OSC with an XY pad (TouchOSC Layout 'Mix16', Page 1) + +[backend osc] +detect = on + +[osc touch] +bind = 0.0.0.0 8000 +dest = learn@9000 + +; Pre-declare the fader values so the range mapping is correct +/*/xy = ff 0.0 1.0 0.0 1.0 + +[evdev xbox] +device = /dev/input/event16 + +[midi launch] + +[map] +xbox.EV_ABS.ABS_X > touch./1/xy:1 +xbox.EV_ABS.ABS_Y > touch./1/xy:0 + +xbox.EV_ABS.ABS_X > launch.ch0.note2 +;xbox.EV_ABS.ABS_Y > launch.ch0.note3 + +launch.ch0.note0 <> touch./1/xy:0 +launch.ch0.note1 <> touch./1/xy:1 -- cgit v1.2.3 From f30cf8fc7d7f7c02f7fae4fa113ef32fc790c31a Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 27 Jul 2019 19:44:52 +0200 Subject: Fix uninitialized value --- backends/osc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/osc.c b/backends/osc.c index 3f19abf..18c8bad 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -499,7 +499,7 @@ static int osc_register_pattern(osc_instance_data* data, char* pattern_path, cha static int osc_configure_instance(instance* inst, char* option, char* value){ osc_instance_data* data = (osc_instance_data*) inst->impl; - char* host = NULL, *port = NULL, *token = NULL; + char* host = NULL, *port = NULL; if(!strcmp(option, "root")){ if(osc_path_validate(value, 0)){ @@ -708,7 +708,7 @@ 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; + int rv = 0; osc_channel_ident ident = { .label = 0 }; -- cgit v1.2.3 From 0bb690437795f1be0f68c562fc2b9dbc66bc97a8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 27 Jul 2019 19:56:22 +0200 Subject: Add flying faders to readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9647d4f..d64190f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ on any other (or the same) supported protocol, for example to: * Translate MIDI Control Changes into Notes ([Example configuration](configs/unifest-17.cfg)) * Translate MIDI Notes into ArtNet or sACN ([Example configuration](configs/launchctl-sacn.cfg)) * Translate OSC messages into MIDI ([Example configuration](configs/midi-osc.cfg)) -* Dynamically route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller +* Dynamically route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller or run effects on TouchOSC (Flying faders demo [configuration](configs/flying-faders.cfg) and [script](configs/flying-faders.lua)) * 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 ([Example configuration](configs/evdev.conf)) -- cgit v1.2.3 From aef72140273b3d98a4d86a42f0f3bcce6d5899ca Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 28 Jul 2019 23:19:54 +0200 Subject: Fix Lua timing on non-linux --- backends/lua.c | 11 +++++++++-- backends/osc.md | 3 +++ configs/midi-osc.cfg | 19 ++----------------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index 61e4e08..4a910a2 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -53,9 +53,16 @@ int init(){ } static uint32_t lua_interval(){ - //FIXME Return delta for next timer here + size_t n = 0; + uint64_t next_timer = 1000; + if(timer_interval){ - return timer_interval; + for(n = 0; n < timers; n++){ + if(timer[n].interval && timer[n].interval - timer[n].delta < next_timer){ + next_timer = timer[n].interval - timer[n].delta; + } + } + return next_timer; } return 1000; } diff --git a/backends/osc.md b/backends/osc.md index b7ce527..1446e06 100644 --- a/backends/osc.md +++ b/backends/osc.md @@ -58,6 +58,9 @@ To configure a range of faders, an expression similar to the following line coul /1/fader* = f 0.0 1.0 ``` +When matching channels against the patterns to use, the first matching pattern (in the order in which they have been configured) will be used +as configuration for that channel. + #### Channel specification A channel may be any valid OSC path, to which the instance root will be prepended if diff --git a/configs/midi-osc.cfg b/configs/midi-osc.cfg index 1b3ccd6..7753a24 100644 --- a/configs/midi-osc.cfg +++ b/configs/midi-osc.cfg @@ -13,23 +13,8 @@ dest = learn@8000 root = /4 ; Pre-declare the fader values so the range mapping is correct -/fader1 = f 0.0 1.0 -/fader2 = f 0.0 1.0 -/fader3 = f 0.0 1.0 -/fader4 = f 0.0 1.0 -/fader5 = f 0.0 1.0 -/fader6 = f 0.0 1.0 -/fader7 = f 0.0 1.0 -/fader8 = f 0.0 1.0 - -/multifader1/1 = f 0.0 1.0 -/multifader1/2 = f 0.0 1.0 -/multifader1/3 = f 0.0 1.0 -/multifader1/4 = f 0.0 1.0 -/multifader1/5 = f 0.0 1.0 -/multifader1/6 = f 0.0 1.0 -/multifader1/7 = f 0.0 1.0 -/multifader1/8 = f 0.0 1.0 +/fader* = f 0.0 1.0 +/multifader1/* = f 0.0 1.0 [midi bcf] read = BCF -- cgit v1.2.3 From 4e604b493e0dc855b6ea6978e5cf8e2de5d2b8d5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 28 Jul 2019 23:40:40 +0200 Subject: Fix evdev relative axes, add detect option --- TODO | 1 - backends/evdev.c | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++----- backends/evdev.h | 8 ++++++++ backends/evdev.md | 18 ++++++++++++------ monster.cfg | 4 +++- 5 files changed, 75 insertions(+), 13 deletions(-) diff --git a/TODO b/TODO index 2a97c30..5f4ce91 100644 --- a/TODO +++ b/TODO @@ -3,5 +3,4 @@ Note source in channel value struct Optimize core channel search (store backend offset) Printing backend / Verbose mode -evdev relative axis size mm_managed_fd.impl is not freed currently diff --git a/backends/evdev.c b/backends/evdev.c index ca8469a..565eb17 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -27,6 +27,12 @@ typedef union { uint64_t label; } evdev_channel_ident; +static struct { + uint8_t detect; +} evdev_config = { + .detect = 0 +}; + int init(){ backend evdev = { .name = BACKEND_NAME, @@ -49,7 +55,15 @@ int init(){ } static int evdev_configure(char* option, char* value) { - fprintf(stderr, "The evdev backend does not take any global configuration\n"); + if(!strcmp(option, "detect")){ + evdev_config.detect = 1; + if(!strcmp(value, "off")){ + evdev_config.detect = 0; + } + return 0; + } + + fprintf(stderr, "Unknown configuration option %s for evdev backend\n", option); return 1; } @@ -185,6 +199,22 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { data->exclusive = 1; return 0; } + else if(!strncmp(option, "relaxis.", 8)){ + data->relative_axis = realloc(data->relative_axis, (data->relative_axes + 1) * sizeof(evdev_relaxis_config)); + if(!data->relative_axis){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + data->relative_axis[data->relative_axes].code = libevdev_event_code_from_name(EV_REL, option + 8); + data->relative_axis[data->relative_axes].max = strtoul(value, &next_token, 0); + data->relative_axis[data->relative_axes].current = strtoul(next_token, NULL, 0); + if(data->relative_axis[data->relative_axes].code < 0){ + fprintf(stderr, "Failed to configure relative axis extents for %s.%s\n", inst->name, option + 8); + return 1; + } + data->relative_axes++; + return 0; + } #ifndef EVDEV_NO_UINPUT else if(!strcmp(option, "output")){ data->output_enabled = 1; @@ -214,7 +244,7 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { return 0; } #endif - fprintf(stderr, "Unknown configuration parameter %s for evdev backend\n", option); + fprintf(stderr, "Unknown instance configuration parameter %s for evdev instance %s\n", option, inst->name); return 1; } @@ -275,21 +305,32 @@ static int evdev_push_event(instance* inst, evdev_instance_data* data, struct in .fields.code = event.code }; channel* chan = mm_channel(inst, ident.label, 0); + size_t axis; if(chan){ val.raw.u64 = event.value; switch(event.type){ case EV_REL: - val.normalised = 0.5 + ((event.value < 0) ? 0.5 : -0.5); + for(axis = 0; axis < data->relative_axes; axis++){ + if(data->relative_axis[axis].code == event.code){ + data->relative_axis[axis].current = clamp(data->relative_axis[axis].current + event.value, data->relative_axis[axis].max, 0); + val.normalised = (double) data->relative_axis[axis].current / (double) data->relative_axis[axis].max; + break; + } + } + if(axis == data->relative_axes){ + val.normalised = 0.5 + ((event.value < 0) ? 0.5 : -0.5); + break; + } break; case EV_ABS: range = libevdev_get_abs_maximum(data->input_ev, event.code) - libevdev_get_abs_minimum(data->input_ev, event.code); - val.normalised = (event.value - libevdev_get_abs_minimum(data->input_ev, event.code)) / (double) range; + val.normalised = clamp((event.value - libevdev_get_abs_minimum(data->input_ev, event.code)) / (double) range, 1.0, 0.0); break; case EV_KEY: case EV_SW: default: - val.normalised = 1.0 * event.value; + val.normalised = clamp(1.0 * event.value, 1.0, 0.0); break; } @@ -299,6 +340,10 @@ static int evdev_push_event(instance* inst, evdev_instance_data* data, struct in } } + if(evdev_config.detect){ + fprintf(stderr, "Incoming evdev data for channel %s.%s.%s\n", inst->name, libevdev_event_type_get_name(event.type), libevdev_event_code_get_name(event.type, event.code)); + } + return 0; } @@ -470,6 +515,8 @@ static int evdev_shutdown(){ libevdev_free(data->output_proto); #endif + data->relative_axes = 0; + free(data->relative_axis); free(data); } diff --git a/backends/evdev.h b/backends/evdev.h index c6e3a25..d719631 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -24,10 +24,18 @@ static int evdev_shutdown(); #define UINPUT_MAX_NAME_SIZE 512 #endif +typedef struct /*_evdev_relative_axis_config*/ { + int code; + int64_t max; + int64_t current; +} evdev_relaxis_config; + typedef struct /*_evdev_instance_model*/ { int input_fd; struct libevdev* input_ev; int exclusive; + size_t relative_axes; + evdev_relaxis_config* relative_axis; int output_enabled; #ifndef EVDEV_NO_UINPUT diff --git a/backends/evdev.md b/backends/evdev.md index d750f1e..995b44c 100644 --- a/backends/evdev.md +++ b/backends/evdev.md @@ -7,7 +7,9 @@ This functionality may require elevated privileges (such as special group member #### Global configuration -This backend does not take any global configuration. +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `detect` | `on` | `off` | Output the channel specification for all events coming in on configured instances to help with configuration. | #### Instance configuration @@ -18,7 +20,8 @@ This backend does not take any global configuration. | `output` | `My Input Device` | none | Output device presentation name. Setting this option enables the instance for output | | `exclusive` | `1` | `0` | Prevent other processes from using the device | | `id` | `0x1 0x2 0x3` | none | Set output device bus identification (Vendor, Product and Version), optional | -| `axis.AXISNAME`| `34300 0 65536 255 4095` | none | Specify absolute axis details (see below) for output. This is required for any absolute axis to be output. +| `axis.AXISNAME`| `34300 0 65536 255 4095` | none | Specify absolute axis details (see below) for output. This is required for any absolute axis to be output. | +| `relaxis.AXISNAME`| `65534 32767` | none | Specify relative axis details (extent and optional initial value) for output and input (see below). | The absolute axis details configuration (e.g. `axis.ABS_X`) is required for any absolute axis on output-enabled instances. The configuration value contains, space-separated, the following values: @@ -34,6 +37,13 @@ If an axis is not used for output, this configuration can be omitted. For real devices, all of these parameters for every axis can be found by running `evtest` on the device. +To use the input from relative axes in absolute-value based protocols, the backend needs a reference frame to +convert the relative movements to absolute values. + +If relative axes are used without specifying their extents, the channel will generate normalized values +of `0`, `0.5` and `1` for any input less than, equal to and greater than `0`, respectively. As for output, only +the values `-1`, `0` and `1` are generated for the same interval. + #### Channel specification A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes @@ -64,10 +74,6 @@ Input devices may synchronize logically connected event types (for example, X an events. The MIDIMonster also generates these events after processing channel events, but may not keep the original event grouping. -Relative axes (`EV_REL`-type events), such as generated by mouses, are currently handled in a very basic fashion, -generating only the normalized channel values of `0`, `0.5` and `1` for any input less than, equal to and greater -than `0`, respectively. As for output, only the values `-1`, `0` and `1` are generated for the same interval. - `EV_KEY` key-down events are sent for normalized channel values over `0.9`. Extended event type values such as `EV_LED`, `EV_SND`, etc are recognized in the MIDIMonster configuration file diff --git a/monster.cfg b/monster.cfg index 7db3ec3..2e6f76f 100644 --- a/monster.cfg +++ b/monster.cfg @@ -1,6 +1,8 @@ [backend artnet] bind = 0.0.0.0 +[backend evdev] + [artnet art] universe = 0 dest = 255.255.255.255 @@ -11,6 +13,6 @@ input = TPPS [loopback loop] [map] +mouse.EV_REL.REL_X > loop.chan0 art.{3..4}{4..3} > loop.chan{4..3}{3..4} art.{1..10} > loop.data{1..10} -art.{500..599} > loop.test{500..599} -- cgit v1.2.3 From a23ce7718ab61e73586d2738329d6bbace0b764b Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 1 Aug 2019 20:54:17 +0200 Subject: Implement evdev relative axis inversion --- backends/evdev.c | 15 ++++++++++++++- backends/evdev.h | 1 + backends/evdev.md | 8 +++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/backends/evdev.c b/backends/evdev.c index 565eb17..7a7913d 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -205,8 +205,13 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { fprintf(stderr, "Failed to allocate memory\n"); return 1; } + data->relative_axis[data->relative_axes].inverted = 0; data->relative_axis[data->relative_axes].code = libevdev_event_code_from_name(EV_REL, option + 8); - data->relative_axis[data->relative_axes].max = strtoul(value, &next_token, 0); + data->relative_axis[data->relative_axes].max = strtoll(value, &next_token, 0); + if(data->relative_axis[data->relative_axes].max < 0){ + data->relative_axis[data->relative_axes].max *= -1; + data->relative_axis[data->relative_axes].inverted = 1; + } data->relative_axis[data->relative_axes].current = strtoul(next_token, NULL, 0); if(data->relative_axis[data->relative_axes].code < 0){ fprintf(stderr, "Failed to configure relative axis extents for %s.%s\n", inst->name, option + 8); @@ -313,6 +318,9 @@ static int evdev_push_event(instance* inst, evdev_instance_data* data, struct in case EV_REL: for(axis = 0; axis < data->relative_axes; axis++){ if(data->relative_axis[axis].code == event.code){ + if(data->relative_axis[axis].inverted){ + event.value *= -1; + } data->relative_axis[axis].current = clamp(data->relative_axis[axis].current + event.value, data->relative_axis[axis].max, 0); val.normalised = (double) data->relative_axis[axis].current / (double) data->relative_axis[axis].max; break; @@ -374,6 +382,11 @@ static int evdev_handle(size_t num, managed_fd* fds){ read_flags = LIBEVDEV_READ_FLAG_SYNC; } + //exclude synchronization events + if(ev.type == EV_SYN){ + continue; + } + //handle event if(evdev_push_event(inst, data, ev)){ return 1; diff --git a/backends/evdev.h b/backends/evdev.h index d719631..f89e362 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -25,6 +25,7 @@ static int evdev_shutdown(); #endif typedef struct /*_evdev_relative_axis_config*/ { + uint8_t inverted; int code; int64_t max; int64_t current; diff --git a/backends/evdev.md b/backends/evdev.md index 995b44c..88dfc85 100644 --- a/backends/evdev.md +++ b/backends/evdev.md @@ -38,12 +38,18 @@ If an axis is not used for output, this configuration can be omitted. For real devices, all of these parameters for every axis can be found by running `evtest` on the device. To use the input from relative axes in absolute-value based protocols, the backend needs a reference frame to -convert the relative movements to absolute values. +convert the relative movements to absolute values. To invert the mapping of the relative axis, specify the `max` value +as a negative number, for example: + +``` +relaxis.REL_X = -1024 512 +``` If relative axes are used without specifying their extents, the channel will generate normalized values of `0`, `0.5` and `1` for any input less than, equal to and greater than `0`, respectively. As for output, only the values `-1`, `0` and `1` are generated for the same interval. + #### Channel specification A channel is specified by its event type and event code, separated by `.`. For a complete list of event types and codes -- cgit v1.2.3 From bbeade8898200a8024169ece30c620016fd5eaf1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 1 Aug 2019 20:58:21 +0200 Subject: Implement midi backend detect option --- README.md | 2 +- backends/evdev.md | 2 +- backends/midi.c | 41 +++++++++++++++++++++++++++++++++++------ backends/midi.md | 1 + 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d64190f..8a0d7f9 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ on any other (or the same) supported protocol, for example to: * Dynamically route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller or run effects on TouchOSC (Flying faders demo [configuration](configs/flying-faders.cfg) and [script](configs/flying-faders.lua)) * 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 ([Example configuration](configs/evdev.conf)) +* Control lighting fixtures or DAWs using gamepad controllers, trackballs, etc ([Example configuration](configs/evdev.conf)) * Play games or type using MIDI controllers [![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster) [![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) diff --git a/backends/evdev.md b/backends/evdev.md index 88dfc85..d57201d 100644 --- a/backends/evdev.md +++ b/backends/evdev.md @@ -9,7 +9,7 @@ This functionality may require elevated privileges (such as special group member | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `detect` | `on` | `off` | Output the channel specification for all events coming in on configured instances to help with configuration. | +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | #### Instance configuration diff --git a/backends/midi.c b/backends/midi.c index 2999e6b..c1480c0 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -31,6 +31,12 @@ enum /*_midi_channel_type*/ { sysmsg }; +static struct { + uint8_t detect; +} midi_config = { + .detect = 0 +}; + int init(){ backend midi = { .name = BACKEND_NAME, @@ -55,8 +61,8 @@ int init(){ return 1; } - snd_seq_nonblock(sequencer, 1); - + snd_seq_nonblock(sequencer, 1); + fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer)); return 0; } @@ -70,6 +76,14 @@ static int midi_configure(char* option, char* value){ return 0; } + if(!strcmp(option, "detect")){ + midi_config.detect = 1; + if(!strcmp(value, "off")){ + midi_config.detect = 0; + } + return 0; + } + fprintf(stderr, "Unknown MIDI backend option %s\n", option); return 1; } @@ -214,7 +228,7 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ snd_seq_ev_set_source(&ev, data->port); snd_seq_ev_set_subs(&ev); snd_seq_ev_set_direct(&ev); - + switch(ident.fields.type){ case note: snd_seq_ev_set_noteon(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); @@ -224,7 +238,6 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ break; case pressure: snd_seq_ev_set_keypress(&ev, ident.fields.channel, ident.fields.control, v[u].normalised * 127.0); - break; case pitchbend: snd_seq_ev_set_pitchbend(&ev, ident.fields.channel, (v[u].normalised * 16383.0) - 8192); @@ -249,6 +262,7 @@ static int midi_handle(size_t num, managed_fd* fds){ instance* inst = NULL; channel* changed = NULL; channel_value val; + char* event_type = NULL; midi_channel_ident ident = { .label = 0 }; @@ -258,6 +272,7 @@ static int midi_handle(size_t num, managed_fd* fds){ } while(snd_seq_event_input(sequencer, &ev) > 0){ + event_type = NULL; ident.label = 0; switch(ev->type){ case SND_SEQ_EVENT_NOTEON: @@ -267,22 +282,26 @@ static int midi_handle(size_t num, managed_fd* fds){ 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 = "note"; break; case SND_SEQ_EVENT_KEYPRESS: ident.fields.type = pressure; ident.fields.channel = ev->data.note.channel; ident.fields.control = ev->data.note.note; val.normalised = (double)ev->data.note.velocity / 127.0; + event_type = "pressure"; break; case SND_SEQ_EVENT_CHANPRESS: ident.fields.type = aftertouch; ident.fields.channel = ev->data.control.channel; val.normalised = (double)ev->data.control.value / 127.0; + event_type = "aftertouch"; break; case SND_SEQ_EVENT_PITCHBEND: ident.fields.type = pitchbend; ident.fields.channel = ev->data.control.channel; val.normalised = ((double)ev->data.control.value + 8192) / 16383.0; + event_type = "pitch"; break; case SND_SEQ_EVENT_CONTROLLER: ident.fields.type = cc; @@ -290,6 +309,7 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.fields.control = ev->data.control.param; val.raw.u64 = ev->data.control.value; val.normalised = (double)ev->data.control.value / 127.0; + event_type = "cc"; break; case SND_SEQ_EVENT_CONTROL14: case SND_SEQ_EVENT_NONREGPARAM: @@ -318,6 +338,15 @@ static int midi_handle(size_t num, managed_fd* fds){ return 1; } } + + if(midi_config.detect && event_type){ + if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){ + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s\n", inst->name, ident.fields.channel, event_type); + } + else{ + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d\n", inst->name, ident.fields.channel, event_type, ident.fields.control); + } + } } free(ev); return 0; @@ -375,13 +404,13 @@ static int midi_start(){ } //register all fds to core - nfds = snd_seq_poll_descriptors_count(sequencer, POLLIN | POLLOUT); + nfds = snd_seq_poll_descriptors_count(sequencer, POLLIN | POLLOUT); pfds = calloc(nfds, sizeof(struct pollfd)); if(!pfds){ fprintf(stderr, "Failed to allocate memory\n"); goto bail; } - nfds = snd_seq_poll_descriptors(sequencer, pfds, nfds, POLLIN | POLLOUT); + nfds = snd_seq_poll_descriptors(sequencer, pfds, nfds, POLLIN | POLLOUT); fprintf(stderr, "MIDI backend registering %d descriptors to core\n", nfds); for(p = 0; p < nfds; p++){ diff --git a/backends/midi.md b/backends/midi.md index 9733cc2..5e295ee 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -7,6 +7,7 @@ The MIDI backend provides read-write access to the MIDI protocol via virtual por | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| | `name` | `MIDIMonster` | none | MIDI client name | +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | #### Instance configuration -- cgit v1.2.3 From 20a6882a063404858588596bd3f12bdd9e53460a Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 3 Aug 2019 18:42:39 +0200 Subject: Windows build compatiblity --- .gitignore | 3 +++ Makefile | 23 +++++++++++++++++-- backend.c | 31 ++++++++++++++----------- backend.h | 9 +++++++- backends/Makefile | 26 +++++++++++++++++++-- backends/artnet.c | 16 ++++++++----- backends/artnet.h | 2 ++ backends/libmmbackend.c | 12 ++++++++-- backends/libmmbackend.h | 6 +++++ backends/lua.c | 4 ---- backends/osc.c | 18 +++++++++------ backends/osc.h | 2 ++ backends/sacn.c | 28 ++++++++++++++--------- backends/sacn.h | 1 - config.c | 56 +++++++++++++++++++++++++++++++++++++++++++++ midimonster.c | 39 ++++++++++++++++++++++++++------ midimonster.h | 38 ++++++++++++++++++++++++------- monster.cfg | 12 +++------- plugin.c | 60 +++++++++++++++++++++++++++++++++++++++++++++---- portability.h | 17 ++++++++++++++ 20 files changed, 326 insertions(+), 77 deletions(-) diff --git a/.gitignore b/.gitignore index 3afd872..4396a38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ midimonster +midimonster.exe +libmmapi.a *.swp *.o *.so +*.dll diff --git a/Makefile b/Makefile index 5ee9cd9..b82d6d8 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,14 @@ -.PHONY: all clean run sanitize backends full backends-full +.PHONY: all clean run sanitize backends windows full backends-full OBJS = config.o backend.o plugin.o PLUGINDIR = "\"./backends/\"" +PLUGINDIR_W32 = "\"backends\\\\\"" SYSTEM := $(shell uname -s) -CFLAGS ?= -g -Wall +CFLAGS ?= -g -Wall -Wpedantic +# Hide all non-API symbols for export +CFLAGS += -fvisibility=hidden + #CFLAGS += -DDEBUG midimonster: LDLIBS = -ldl midimonster: CFLAGS += -DPLUGINS=$(PLUGINDIR) @@ -21,6 +25,8 @@ all: midimonster backends full: midimonster backends-full +windows: midimonster.exe + backends: $(MAKE) -C backends @@ -31,8 +37,21 @@ backends-full: midimonster: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ +midimonster.exe: export CC = x86_64-w64-mingw32-gcc +#midimonster.exe: CFLAGS += -Wno-format +midimonster.exe: CFLAGS += -DPLUGINS=$(PLUGINDIR_W32) -Wno-format +midimonster.exe: LDLIBS = -lws2_32 +midimonster.exe: LDFLAGS += -Wl,--out-implib,libmmapi.a +midimonster.exe: midimonster.c portability.h $(OBJS) + $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ + # The windows build for backends requires the import library generated with the build, + # so the backends can't be a prerequisite for the executable... + $(MAKE) -C backends windows + clean: $(RM) midimonster + $(RM) midimonster.exe + $(RM) libmmapi.a $(RM) $(OBJS) $(MAKE) -C backends clean diff --git a/backend.c b/backend.c index 5df5d73..4fa7704 100644 --- a/backend.c +++ b/backend.c @@ -1,4 +1,9 @@ #include +#ifndef _WIN32 +#define MM_API __attribute__((visibility ("default"))) +#else +#define MM_API __attribute__((dllexport)) +#endif #include "midimonster.h" #include "backend.h" @@ -26,7 +31,7 @@ int backends_handle(size_t nfds, managed_fd* fds){ } } - DBGPF("Notifying backend %s of %zu waiting FDs\n", backends[u].name, n); + 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); @@ -59,28 +64,28 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ } } - DBGPF("Calling handler for instance %s with %zu events\n", instances[u]->name, 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); } return 0; } -channel* mm_channel(instance* i, uint64_t ident, uint8_t create){ +channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create){ size_t u; for(u = 0; u < nchannels; u++){ - if(channels[u]->instance == i && channels[u]->ident == ident){ - DBGPF("Requested channel %zu on instance %s already exists, reusing\n", ident, i->name); + 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]; } } if(!create){ - DBGPF("Requested unknown channel %zu on instance %s\n", ident, i->name); + DBGPF("Requested unknown channel %lu on instance %s\n", ident, inst->name); return NULL; } - DBGPF("Creating previously unknown channel %zu on instance %s\n", ident, i->name); + 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){ fprintf(stderr, "Failed to allocate memory\n"); @@ -95,12 +100,12 @@ channel* mm_channel(instance* i, uint64_t ident, uint8_t create){ return NULL; } - channels[nchannels]->instance = i; + channels[nchannels]->instance = inst; channels[nchannels]->ident = ident; return channels[nchannels++]; } -instance* mm_instance(){ +instance* MM_API mm_instance(){ instance** new_inst = realloc(instances, (ninstances + 1) * sizeof(instance*)); if(!new_inst){ //TODO free @@ -118,7 +123,7 @@ instance* mm_instance(){ return instances[ninstances++]; } -instance* mm_instance_find(char* name, uint64_t ident){ +instance* MM_API mm_instance_find(char* name, uint64_t ident){ size_t u; backend* b = backend_match(name); if(!b){ @@ -134,7 +139,7 @@ instance* mm_instance_find(char* name, uint64_t ident){ return NULL; } -int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ +int MM_API 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 @@ -177,7 +182,7 @@ void instances_free(){ void channels_free(){ size_t u; for(u = 0; u < nchannels; u++){ - DBGPF("Destroying channel %zu on instance %s\n", channels[u]->ident, channels[u]->instance->name); + 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]); } @@ -232,7 +237,7 @@ struct timeval backend_timeout(){ return tv; } -int mm_backend_register(backend b){ +int MM_API mm_backend_register(backend b){ if(!backend_match(b.name)){ backends = realloc(backends, (nbackends + 1) * sizeof(backend)); if(!backends){ diff --git a/backend.h b/backend.h index daf96bc..7529154 100644 --- a/backend.h +++ b/backend.h @@ -1,8 +1,8 @@ #include +/* Internal API */ int backends_handle(size_t nfds, managed_fd* fds); int backends_notify(size_t nev, channel** c, channel_value* v); - backend* backend_match(char* name); instance* instance_match(char* name); struct timeval backend_timeout(); @@ -10,3 +10,10 @@ int backends_start(); int backends_stop(); void instances_free(); void channels_free(); + +/* Backend API */ +channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create); +instance* MM_API mm_instance(); +instance* MM_API mm_instance_find(char* name, uint64_t ident); +int MM_API mm_backend_instances(char* name, size_t* ninst, instance*** inst); +int MM_API mm_backend_register(backend b); diff --git a/backends/Makefile b/backends/Makefile index 22cb95b..2374df0 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,12 +1,13 @@ .PHONY: all clean full OPTIONAL_BACKENDS = ola.so +WINDOWS_BACKENDS = loopback.dll artnet.dll osc.dll sacn.dll LINUX_BACKENDS = midi.so evdev.so BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) -CFLAGS += -g -fPIC -I../ +CFLAGS += -g -fPIC -I../ -Wall -Wpedantic CPPFLAGS += -g -fPIC -I../ LDFLAGS += -shared @@ -20,8 +21,17 @@ LDFLAGS += -undefined dynamic_lookup endif artnet.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +artnet.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +artnet.dll: LDLIBS += -lws2_32 + osc.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +osc.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +osc.dll: LDLIBS += -lws2_32 + sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +sacn.dll: LDLIBS += -lws2_32 + midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) @@ -33,12 +43,24 @@ lua.so: LDLIBS += $(shell pkg-config --libs lua5.3) %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) +%.dll :: %.c %.h $(BACKEND_LIB) + $(CC) $(CFLAGS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) $(LDLIBS) + %.so :: %.cpp %.h $(CXX) $(CPPFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) all: $(BACKEND_LIB) $(BACKENDS) +../libmmapi.a: + $(MAKE) -C ../ midimonster.exe + +windows: export CC = x86_64-w64-mingw32-gcc +windows: LDLIBS += -lmmapi +windows: LDFLAGS += -L../ +windows: CFLAGS += -Wno-format -Wno-pointer-sign +windows: ../libmmapi.a $(BACKEND_LIB) $(WINDOWS_BACKENDS) + full: $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) clean: - $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) + $(RM) $(BACKEND_LIB) $(BACKENDS) $(OPTIONAL_BACKENDS) $(WINDOWS_BACKENDS) diff --git a/backends/artnet.c b/backends/artnet.c index 8b404a6..a6df4ab 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -3,8 +3,8 @@ #include #include "libmmbackend.h" - #include "artnet.h" + #define MAX_FDS 255 #define BACKEND_NAME "artnet" @@ -32,7 +32,7 @@ static int artnet_listener(char* host, char* port){ return -1; } - fprintf(stderr, "ArtNet backend interface %zu bound to %s port %s\n", artnet_fds, host, port); + fprintf(stderr, "ArtNet backend interface %lu bound to %s port %s\n", artnet_fds, host, port); artnet_fd[artnet_fds].fd = fd; artnet_fd[artnet_fds].output_instances = 0; artnet_fd[artnet_fds].output_instance = NULL; @@ -212,7 +212,7 @@ static int artnet_transmit(instance* inst){ }; memcpy(frame.data, data->data.out, 512); - if(sendto(artnet_fd[data->fd_index].fd, &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ + if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ fprintf(stderr, "Failed to output ArtNet frame for instance %s: %s\n", inst->name, strerror(errno)); } @@ -230,7 +230,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) artnet_instance_data* data = (artnet_instance_data*) inst->impl; if(!data->dest_len){ - fprintf(stderr, "ArtNet instance %s not enabled for output (%zu channel events)\n", inst->name, num); + fprintf(stderr, "ArtNet instance %s not enabled for output (%lu channel events)\n", inst->name, num); return 0; } @@ -295,7 +295,7 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ } if(!chan){ - fprintf(stderr, "Active channel %zu on %s not known to core\n", p, inst->name); + fprintf(stderr, "Active channel %lu on %s not known to core\n", p, inst->name); return 1; } @@ -367,7 +367,11 @@ static int artnet_handle(size_t num, managed_fd* fds){ } } while(bytes_read > 0); + #ifdef _WIN32 + if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){ + #else if(bytes_read < 0 && errno != EAGAIN){ + #endif fprintf(stderr, "ArtNet failed to receive data: %s\n", strerror(errno)); } @@ -438,7 +442,7 @@ static int artnet_start(){ } } - fprintf(stderr, "ArtNet backend registering %zu descriptors to core\n", artnet_fds); + fprintf(stderr, "ArtNet backend registering %lu descriptors to core\n", artnet_fds); for(u = 0; u < artnet_fds; u++){ if(mm_manage_fd(artnet_fd[u].fd, BACKEND_NAME, 1, (void*) u)){ goto bail; diff --git a/backends/artnet.h b/backends/artnet.h index 90aedd5..f5aa745 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -1,4 +1,6 @@ +#ifndef _WIN32 #include +#endif #include "midimonster.h" int init(); diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 6320611..b27ebc5 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -53,7 +53,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad } int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ - int fd = -1, status, yes = 1, flags; + int fd = -1, status, yes = 1; struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = socktype, @@ -106,12 +106,20 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ } //set nonblocking - flags = fcntl(fd, F_GETFL, 0); + #ifdef _WIN32 + u_long mode = 1; + if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){ + closesocket(fd); + return 1; + } + #else + int flags = fcntl(fd, F_GETFL, 0); if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ fprintf(stderr, "Failed to set socket nonblocking\n"); close(fd); return -1; } + #endif return fd; } diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 38bfca0..77cad6a 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -1,14 +1,20 @@ #include #include #include +#ifdef _WIN32 +#include +//#define close closesocket +#else #include #include +#endif #include #include #include #include #include #include +#include "../portability.h" /* Parse spec as host specification in the form * host port diff --git a/backends/lua.c b/backends/lua.c index 4a910a2..ec02575 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -161,8 +161,6 @@ static int lua_callback_output(lua_State* interpreter){ static int lua_callback_interval(lua_State* interpreter){ size_t n = 0; - instance* inst = NULL; - lua_instance_data* data = NULL; uint64_t interval = 0; int reference = LUA_NOREF; @@ -174,8 +172,6 @@ static int lua_callback_interval(lua_State* interpreter){ //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 and round the interval interval = luaL_checkinteger(interpreter, 2); diff --git a/backends/osc.c b/backends/osc.c index 18c8bad..03e431f 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -1,8 +1,8 @@ #include #include #include -#include "libmmbackend.h" +#include "libmmbackend.h" #include "osc.h" /* @@ -480,14 +480,14 @@ static int osc_register_pattern(osc_instance_data* data, char* pattern_path, cha //parse min/max values token = strtok(NULL, " "); if(!token){ - fprintf(stderr, "Missing minimum specification for parameter %zu of OSC pattern %s\n", u, pattern_path); + fprintf(stderr, "Missing minimum specification for parameter %lu of OSC pattern %s\n", u, pattern_path); return 1; } data->pattern[pattern].min[u] = osc_parse_value_spec(format[u], token); token = strtok(NULL, " "); if(!token){ - fprintf(stderr, "Missing maximum specification for parameter %zu of OSC pattern %s\n", u, pattern_path); + fprintf(stderr, "Missing maximum specification for parameter %lu of OSC pattern %s\n", u, pattern_path); return 1; } data->pattern[pattern].max[u] = osc_parse_value_spec(format[u], token); @@ -689,7 +689,7 @@ static int osc_output_channel(instance* inst, size_t channel){ //write data if(offset + osc_data_length(data->channel[channel].type[p]) >= sizeof(xmit_buf)){ - fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %zu\n", inst->name, data->channel[channel].path, p); + fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %lu\n", inst->name, data->channel[channel].path, p); return 1; } @@ -720,7 +720,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ osc_instance_data* data = (osc_instance_data*) inst->impl; if(!data->dest_len){ - fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%zu channels)\n", inst->name, num); + fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%lu channels)\n", inst->name, num); return 0; } @@ -778,7 +778,7 @@ static int osc_process_packet(instance* inst, char* local_path, char* format, ui channel* chan = NULL; if(payload_len % 4){ - fprintf(stderr, "Invalid OSC packet, data length %zu\n", payload_len); + fprintf(stderr, "Invalid OSC packet, data length %lu\n", payload_len); return 0; } @@ -877,7 +877,11 @@ static int osc_handle(size_t num, managed_fd* fds){ } } while(bytes_read > 0); + #ifdef _WIN32 + if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){ + #else if(bytes_read < 0 && errno != EAGAIN){ + #endif fprintf(stderr, "OSC failed to receive data for instance %s: %s\n", inst->name, strerror(errno)); } @@ -924,7 +928,7 @@ static int osc_start(){ } } - fprintf(stderr, "OSC backend registered %zu descriptors to core\n", fds); + fprintf(stderr, "OSC backend registered %lu descriptors to core\n", fds); free(inst); return 0; diff --git a/backends/osc.h b/backends/osc.h index 4e9dec5..b2aaea7 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -1,6 +1,8 @@ #include "midimonster.h" #include +#ifndef _WIN32 #include +#endif #define OSC_RECV_BUF 8192 #define OSC_XMIT_BUF 8192 diff --git a/backends/sacn.c b/backends/sacn.c index 75bb76f..6f7d1a5 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -1,16 +1,18 @@ #include #include -#include -#include #include #include #include #include +#ifndef _WIN32 +#include #include +#include +#endif #include "libmmbackend.h" - #include "sacn.h" + //upper limit imposed by using the fd index as 16-bit part of the instance id #define MAX_FDS 4096 #define BACKEND_NAME "sacn" @@ -71,7 +73,7 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){ return -1; } - fprintf(stderr, "sACN backend interface %zu bound to %s port %s\n", global_cfg.fds, host, port); + fprintf(stderr, "sACN backend interface %lu bound to %s port %s\n", global_cfg.fds, host, port); global_cfg.fd[global_cfg.fds].fd = fd; global_cfg.fd[global_cfg.fds].flags = fd_flags; global_cfg.fd[global_cfg.fds].universes = 0; @@ -271,7 +273,7 @@ static int sacn_transmit(instance* inst){ memcpy(pdu.data.source_name, global_cfg.source_name, sizeof(pdu.data.source_name)); memcpy((((uint8_t*)pdu.data.data) + 1), data->data.out, 512); - if(sendto(global_cfg.fd[data->fd_index].fd, &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ + if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ fprintf(stderr, "Failed to output sACN frame for instance %s: %s\n", inst->name, strerror(errno)); } @@ -293,7 +295,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ } if(!data->xmit_prio){ - fprintf(stderr, "sACN instance %s not enabled for output (%zu channel events)\n", inst->name, num); + fprintf(stderr, "sACN instance %s not enabled for output (%lu channel events)\n", inst->name, num); return 0; } @@ -378,7 +380,7 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame } if(!chan){ - fprintf(stderr, "Active channel %zu on %s not known to core", u, inst->name); + fprintf(stderr, "Active channel %lu on %s not known to core", u, inst->name); return 1; } @@ -445,8 +447,8 @@ static void sacn_discovery(size_t fd){ pdu.data.page = page; memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t)); - if(sendto(global_cfg.fd[fd].fd, &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ - fprintf(stderr, "Failed to output sACN universe discovery frame for interface %zu: %s\n", fd, strerror(errno)); + if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ + fprintf(stderr, "Failed to output sACN universe discovery frame for interface %lu: %s\n", fd, strerror(errno)); } } } @@ -512,7 +514,11 @@ static int sacn_handle(size_t num, managed_fd* fds){ } } while(bytes_read > 0); + #ifdef _WIN32 + if(bytes_read < 0 && WSAGetLastError() != WSAEWOULDBLOCK){ + #else if(bytes_read < 0 && errno != EAGAIN){ + #endif fprintf(stderr, "sACN failed to receive data: %s\n", strerror(errno)); } @@ -577,7 +583,7 @@ static int sacn_start(){ if(!data->unicast_input){ mcast_req.imr_multiaddr.s_addr = htobe32(((uint32_t) 0xefff0000) | ((uint32_t) data->uni)); - if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mcast_req, sizeof(mcast_req))){ + if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ fprintf(stderr, "Failed to join Multicast group for sACN universe %u on instance %s: %s\n", data->uni, inst[u]->name, strerror(errno)); } } @@ -604,7 +610,7 @@ static int sacn_start(){ } } - fprintf(stderr, "sACN backend registering %zu descriptors to core\n", global_cfg.fds); + fprintf(stderr, "sACN backend registering %lu descriptors to core\n", global_cfg.fds); for(u = 0; u < global_cfg.fds; u++){ //allocate memory for storing last frame transmission timestamp global_cfg.fd[u].last_frame = calloc(global_cfg.fd[u].universes, sizeof(uint64_t)); diff --git a/backends/sacn.h b/backends/sacn.h index e7106f7..7af2a36 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -1,4 +1,3 @@ -#include #include "midimonster.h" int init(); diff --git a/config.c b/config.c index 6d5fd16..93fb56d 100644 --- a/config.c +++ b/config.c @@ -20,6 +20,62 @@ typedef enum { static backend* current_backend = NULL; static instance* current_instance = NULL; +#ifdef _WIN32 +#define GETLINE_BUFFER 4096 + +static ssize_t getline(char** line, size_t* alloc, FILE* stream){ + size_t bytes_read = 0; + char c; + //sanity checks + if(!line || !alloc || !stream){ + return -1; + } + + //allocate buffer if none provided + if(!*line || !*alloc){ + *alloc = GETLINE_BUFFER; + *line = calloc(GETLINE_BUFFER, sizeof(char)); + if(!*line){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + } + + if(feof(stream)){ + return -1; + } + + for(c = fgetc(stream); 1; c = fgetc(stream)){ + //end of buffer, resize + if(bytes_read == (*alloc) - 1){ + *alloc += GETLINE_BUFFER; + *line = realloc(*line, (*alloc) * sizeof(char)); + if(!*line){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + } + + //store character + (*line)[bytes_read] = c; + + //end of line + if(feof(stream) || c == '\n'){ + //terminate string + (*line)[bytes_read + 1] = 0; + return bytes_read; + } + + //input broken + if(ferror(stream) || c < 0){ + return -1; + } + + bytes_read++; + } +} +#endif + static char* config_trim_line(char* in){ ssize_t n; //trim front diff --git a/midimonster.c b/midimonster.c index fb664a4..df27ca3 100644 --- a/midimonster.c +++ b/midimonster.c @@ -1,9 +1,14 @@ #include #include -#include #include #include #include +#ifndef _WIN32 +#include +#define MM_API __attribute__((visibility("default"))) +#else +#define MM_API __attribute__((dllexport)) +#endif #include "midimonster.h" #include "config.h" #include "backend.h" @@ -35,11 +40,14 @@ static void signal_handler(int signum){ shutdown_requested = 1; } -uint64_t mm_timestamp(){ +uint64_t MM_API mm_timestamp(){ return global_timestamp; } static void update_timestamp(){ + #ifdef _WIN32 + global_timestamp = GetTickCount(); + #else struct timespec current; if(clock_gettime(CLOCK_MONOTONIC_COARSE, ¤t)){ fprintf(stderr, "Failed to update global timestamp, time-based processing for some backends may be impaired: %s\n", strerror(errno)); @@ -47,6 +55,7 @@ static void update_timestamp(){ } global_timestamp = current.tv_sec * 1000 + current.tv_nsec / 1000000; + #endif } int mm_map_channel(channel* from, channel* to){ @@ -99,7 +108,7 @@ void map_free(){ map = NULL; } -int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ +int MM_API mm_manage_fd(int new_fd, char* back, int manage, void* impl){ backend* b = backend_match(back); size_t u; @@ -163,7 +172,7 @@ void fds_free(){ fd = NULL; } -int mm_channel_event(channel* c, channel_value v){ +int MM_API mm_channel_event(channel* c, channel_value v){ size_t u, p; //find mapped channels @@ -229,7 +238,7 @@ static fd_set fds_collect(int* max_fd){ *max_fd = -1; } - DBGPF("Building selector set from %zu FDs registered to core\n", fds); + DBGPF("Building selector set from %lu FDs registered to core\n", fds); FD_ZERO(&rv_fds); for(u = 0; u < fds; u++){ if(fd[u].fd >= 0){ @@ -243,6 +252,17 @@ static fd_set fds_collect(int* max_fd){ return rv_fds; } +int platform_initialize(){ +#ifdef _WIN32 + WSADATA wsa; + WORD version = MAKEWORD(2, 2); + if(WSAStartup(version, &wsa)){ + return 1; + } +#endif + return 0; +} + int main(int argc, char** argv){ fd_set all_fds, read_fds; event_collection* secondary = NULL; @@ -255,6 +275,11 @@ int main(int argc, char** argv){ cfg_file = argv[1]; } + if(platform_initialize()){ + fprintf(stderr, "Failed to perform platform-specific initialization\n"); + return EXIT_FAILURE; + } + FD_ZERO(&all_fds); //initialize backends if(plugins_load(PLUGINS)){ @@ -316,14 +341,14 @@ int main(int argc, char** argv){ update_timestamp(); //run backend processing, collect events - DBGPF("%zu backend FDs signaled\n", n); + DBGPF("%lu backend FDs signaled\n", n); if(backends_handle(n, signaled_fds)){ goto bail; } while(primary->n){ //swap primary and secondary event collectors - DBGPF("Swapping event collectors, %zu events in primary\n", primary->n); + 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; diff --git a/midimonster.h b/midimonster.h index 7f70f5b..eb118c6 100644 --- a/midimonster.h +++ b/midimonster.h @@ -4,6 +4,21 @@ #include #include +#ifndef MM_API + #ifdef _WIN32 + #define MM_API __attribute__((dllimport)) + #else + #define MM_API + #endif +#endif + +/* GCC ignores the visibility attributes on some API functions, so override visibility */ +#if !defined(_WIN32) && defined(__GNUC__) && !defined(__clang__) + #undef MM_API + #define MM_API + #pragma GCC visibility push(default) +#endif + /* Straight-forward min / max macros */ #define max(a,b) (((a) > (b)) ? (a) : (b)) #define min(a,b) (((a) < (b)) ? (a) : (b)) @@ -187,7 +202,7 @@ typedef struct /*_mm_channel_mapping*/ { /* * Register a new backend. */ -int mm_backend_register(backend b); +int MM_API mm_backend_register(backend b); /* * Provides a pointer to a newly (zero-)allocated instance. @@ -201,7 +216,8 @@ int mm_backend_register(backend b); * mmbackend_shutdown procedure of the backend, eg. by querying * all instances for the backend. */ -instance* mm_instance(); +instance* MM_API mm_instance(); + /* * Finds an instance matching the specified backend and identifier. * Since setting an identifier for an instance is optional, @@ -209,7 +225,8 @@ instance* mm_instance(); * Instance identifiers may for example be set in the backends * mmbackend_start call. */ -instance* mm_instance_find(char* backend, uint64_t ident); +instance* MM_API mm_instance_find(char* backend, uint64_t ident); + /* * Provides a pointer to a channel structure, pre-filled with * the provided instance reference and identifier. @@ -224,30 +241,35 @@ instance* mm_instance_find(char* backend, uint64_t ident); * this function, the backend will receive a call to its channel_free * function. */ -channel* mm_channel(instance* i, uint64_t ident, uint8_t create); +channel* MM_API mm_channel(instance* i, uint64_t ident, uint8_t create); //TODO channel* mm_channel_find() + /* * 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. */ -int mm_manage_fd(int fd, char* backend, int manage, void* impl); +int MM_API 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. */ -int mm_channel_event(channel* c, channel_value v); +int MM_API mm_channel_event(channel* c, channel_value v); + /* * Query all active instances for a given backend. * *i will need to be freed by the caller. */ -int mm_backend_instances(char* backend, size_t* n, instance*** i); +int MM_API 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. */ -uint64_t mm_timestamp(); +uint64_t MM_API mm_timestamp(); + /* * Create a channel-to-channel mapping. This API should not * be used by backends. It is only exported for core modules. diff --git a/monster.cfg b/monster.cfg index 2e6f76f..2413f6d 100644 --- a/monster.cfg +++ b/monster.cfg @@ -1,18 +1,12 @@ [backend artnet] bind = 0.0.0.0 -[backend evdev] +[loopback loop] [artnet art] universe = 0 dest = 255.255.255.255 -[evdev mouse] -input = TPPS - -[loopback loop] - [map] -mouse.EV_REL.REL_X > loop.chan0 -art.{3..4}{4..3} > loop.chan{4..3}{3..4} -art.{1..10} > loop.data{1..10} + +art.1+2 > loop.b diff --git a/plugin.c b/plugin.c index fc642ac..a452559 100644 --- a/plugin.c +++ b/plugin.c @@ -1,11 +1,20 @@ #include #include -#include #include #include #include #include #include +#include "portability.h" +#ifdef _WIN32 +#define dlclose FreeLibrary +#define dlsym GetProcAddress +#define dlerror() "Failed" +#define dlopen(lib,ig) LoadLibrary(lib) +#else +#include +#endif + #include "plugin.h" static size_t plugins = 0; @@ -14,19 +23,29 @@ static void** plugin_handle = NULL; static int plugin_attach(char* path, char* file){ plugin_init init = NULL; void* handle = NULL; + char* lib = NULL; + char* error = NULL; - char* lib = calloc(strlen(path) + strlen(file) + 1, sizeof(char)); + lib = calloc(strlen(path) + strlen(file) + 1, sizeof(char)); if(!lib){ fprintf(stderr, "Failed to allocate memory\n"); return 1; } - snprintf(lib, strlen(path) + strlen(file) + 1, "%s%s", path, file); handle = dlopen(lib, RTLD_NOW); if(!handle){ - fprintf(stderr, "Failed to load plugin %s: %s\n", lib, dlerror()); + #ifdef _WIN32 + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + #else + error = dlerror(); + #endif + fprintf(stderr, "Failed to load plugin %s: %s\n", lib, error); free(lib); + #ifdef _WIN32 + LocalFree(error); + #endif return 0; } @@ -62,6 +81,38 @@ static int plugin_attach(char* path, char* file){ int plugins_load(char* path){ int rv = -1; +#ifdef _WIN32 + char* search_expression = calloc(strlen(path) + strlen("*.dll") + 1, sizeof(char)); + if(!search_expression){ + fprintf(stderr, "Failed to allocate memory\n"); + return -1; + } + snprintf(search_expression, strlen(path) + strlen("*.dll"), "%s*.dll", path); + + WIN32_FIND_DATA result; + HANDLE hSearch = FindFirstFile(search_expression, &result); + + if(hSearch == INVALID_HANDLE_VALUE){ + LPVOID lpMsgBuf = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL); + fprintf(stderr, "Failed to search for backend plugin files in %s: %s\n", path, lpMsgBuf); + LocalFree(lpMsgBuf); + return -1; + } + + do { + if(plugin_attach(path, result.cFileName)){ + goto load_done; + } + } while(FindNextFile(hSearch, &result)); + + rv = 0; +load_done: + free(search_expression); + FindClose(hSearch); + return rv; +#else struct dirent* entry; struct stat file_stat; DIR* directory = opendir(path); @@ -100,6 +151,7 @@ load_done: return -1; } return rv; +#endif } int plugins_close(){ diff --git a/portability.h b/portability.h index 25aee01..903ecd8 100644 --- a/portability.h +++ b/portability.h @@ -19,3 +19,20 @@ #define be64toh(x) OSSwapBigToHostInt64(x) #define le64toh(x) OSSwapLittleToHostInt64(x) #endif + +#ifdef _WIN32 + #define WIN32_LEAN_AND_MEAN + #include + #include + + #define htobe16(x) htons(x) + #define be16toh(x) ntohs(x) + + #define htobe32(x) htonl(x) + #define be32toh(x) ntohl(x) + + #define htobe64(x) _byteswap_uint64(x) + #define htole64(x) (x) + #define be64toh(x) _byteswap_uint64(x) + #define le64toh(x) (x) +#endif -- cgit v1.2.3 From 47a5f9a21bd661f9161d6175ebd074962daee255 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 7 Aug 2019 17:25:24 +0200 Subject: Fix export visibilities for GCC --- Makefile | 5 +---- backend.c | 10 +++++----- backend.h | 10 +++++----- backends/Makefile | 10 +++++++--- backends/midi.c | 6 ------ midimonster.c | 16 ++++++++-------- midimonster.h | 23 ++++++++--------------- 7 files changed, 34 insertions(+), 46 deletions(-) diff --git a/Makefile b/Makefile index b82d6d8..57fe089 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ all: midimonster backends full: midimonster backends-full windows: midimonster.exe + $(MAKE) -C backends windows backends: $(MAKE) -C backends @@ -38,15 +39,11 @@ midimonster: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ midimonster.exe: export CC = x86_64-w64-mingw32-gcc -#midimonster.exe: CFLAGS += -Wno-format midimonster.exe: CFLAGS += -DPLUGINS=$(PLUGINDIR_W32) -Wno-format midimonster.exe: LDLIBS = -lws2_32 midimonster.exe: LDFLAGS += -Wl,--out-implib,libmmapi.a midimonster.exe: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ - # The windows build for backends requires the import library generated with the build, - # so the backends can't be a prerequisite for the executable... - $(MAKE) -C backends windows clean: $(RM) midimonster diff --git a/backend.c b/backend.c index 4fa7704..3a18f41 100644 --- a/backend.c +++ b/backend.c @@ -71,7 +71,7 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ return 0; } -channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create){ +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){ @@ -105,7 +105,7 @@ channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create){ return channels[nchannels++]; } -instance* MM_API mm_instance(){ +MM_API instance* mm_instance(){ instance** new_inst = realloc(instances, (ninstances + 1) * sizeof(instance*)); if(!new_inst){ //TODO free @@ -123,7 +123,7 @@ instance* MM_API mm_instance(){ return instances[ninstances++]; } -instance* MM_API mm_instance_find(char* name, uint64_t ident){ +MM_API instance* mm_instance_find(char* name, uint64_t ident){ size_t u; backend* b = backend_match(name); if(!b){ @@ -139,7 +139,7 @@ instance* MM_API mm_instance_find(char* name, uint64_t ident){ return NULL; } -int MM_API mm_backend_instances(char* name, size_t* ninst, instance*** inst){ +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 @@ -237,7 +237,7 @@ struct timeval backend_timeout(){ return tv; } -int MM_API mm_backend_register(backend b){ +MM_API int mm_backend_register(backend b){ if(!backend_match(b.name)){ backends = realloc(backends, (nbackends + 1) * sizeof(backend)); if(!backends){ diff --git a/backend.h b/backend.h index 7529154..6573e17 100644 --- a/backend.h +++ b/backend.h @@ -12,8 +12,8 @@ void instances_free(); void channels_free(); /* Backend API */ -channel* MM_API mm_channel(instance* inst, uint64_t ident, uint8_t create); -instance* MM_API mm_instance(); -instance* MM_API mm_instance_find(char* name, uint64_t ident); -int MM_API mm_backend_instances(char* name, size_t* ninst, instance*** inst); -int MM_API mm_backend_register(backend b); +MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create); +MM_API instance* mm_instance(); +MM_API instance* mm_instance_find(char* name, uint64_t ident); +MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst); +MM_API int mm_backend_register(backend b); diff --git a/backends/Makefile b/backends/Makefile index 2374df0..3308ef0 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,8 +1,8 @@ .PHONY: all clean full -OPTIONAL_BACKENDS = ola.so -WINDOWS_BACKENDS = loopback.dll artnet.dll osc.dll sacn.dll LINUX_BACKENDS = midi.so evdev.so -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so +OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o SYSTEM := $(shell uname -s) @@ -32,6 +32,10 @@ sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: LDLIBS += -lws2_32 +maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +maweb.dll: LDLIBS += -lws2_32 + midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) diff --git a/backends/midi.c b/backends/midi.c index c1480c0..9c6ba80 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -14,12 +14,6 @@ typedef union { uint64_t label; } midi_channel_ident; -/* - * TODO - * Optionally send note-off messages - * Optionally send updates as after-touch - */ - enum /*_midi_channel_type*/ { none = 0, note, diff --git a/midimonster.c b/midimonster.c index df27ca3..1e47698 100644 --- a/midimonster.c +++ b/midimonster.c @@ -40,7 +40,7 @@ static void signal_handler(int signum){ shutdown_requested = 1; } -uint64_t MM_API mm_timestamp(){ +MM_API uint64_t mm_timestamp(){ return global_timestamp; } @@ -98,7 +98,7 @@ int mm_map_channel(channel* from, channel* to){ return 0; } -void map_free(){ +static void map_free(){ size_t u; for(u = 0; u < mappings; u++){ free(map[u].to); @@ -108,7 +108,7 @@ void map_free(){ map = NULL; } -int MM_API mm_manage_fd(int new_fd, char* back, int manage, void* impl){ +MM_API int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ backend* b = backend_match(back); size_t u; @@ -158,7 +158,7 @@ int MM_API mm_manage_fd(int new_fd, char* back, int manage, void* impl){ return 0; } -void fds_free(){ +static void fds_free(){ size_t u; for(u = 0; u < fds; u++){ //TODO free impl @@ -172,7 +172,7 @@ void fds_free(){ fd = NULL; } -int MM_API mm_channel_event(channel* c, channel_value v){ +MM_API int mm_channel_event(channel* c, channel_value v){ size_t u, p; //find mapped channels @@ -213,7 +213,7 @@ int MM_API mm_channel_event(channel* c, channel_value v){ return 0; } -void event_free(){ +static void event_free(){ size_t u; for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ @@ -223,7 +223,7 @@ void event_free(){ } } -int usage(char* fn){ +static int usage(char* fn){ fprintf(stderr, "MIDIMonster v0.1\n"); fprintf(stderr, "Usage:\n"); fprintf(stderr, "\t%s \n", fn); @@ -252,7 +252,7 @@ static fd_set fds_collect(int* max_fd){ return rv_fds; } -int platform_initialize(){ +static int platform_initialize(){ #ifdef _WIN32 WSADATA wsa; WORD version = MAKEWORD(2, 2); diff --git a/midimonster.h b/midimonster.h index eb118c6..270a61f 100644 --- a/midimonster.h +++ b/midimonster.h @@ -12,13 +12,6 @@ #endif #endif -/* GCC ignores the visibility attributes on some API functions, so override visibility */ -#if !defined(_WIN32) && defined(__GNUC__) && !defined(__clang__) - #undef MM_API - #define MM_API - #pragma GCC visibility push(default) -#endif - /* Straight-forward min / max macros */ #define max(a,b) (((a) > (b)) ? (a) : (b)) #define min(a,b) (((a) < (b)) ? (a) : (b)) @@ -202,7 +195,7 @@ typedef struct /*_mm_channel_mapping*/ { /* * Register a new backend. */ -int MM_API mm_backend_register(backend b); +MM_API int mm_backend_register(backend b); /* * Provides a pointer to a newly (zero-)allocated instance. @@ -216,7 +209,7 @@ int MM_API mm_backend_register(backend b); * mmbackend_shutdown procedure of the backend, eg. by querying * all instances for the backend. */ -instance* MM_API mm_instance(); +MM_API instance* mm_instance(); /* * Finds an instance matching the specified backend and identifier. @@ -225,7 +218,7 @@ instance* MM_API mm_instance(); * Instance identifiers may for example be set in the backends * mmbackend_start call. */ -instance* MM_API mm_instance_find(char* backend, uint64_t ident); +MM_API instance* mm_instance_find(char* backend, uint64_t ident); /* * Provides a pointer to a channel structure, pre-filled with @@ -241,7 +234,7 @@ instance* MM_API mm_instance_find(char* backend, uint64_t ident); * this function, the backend will receive a call to its channel_free * function. */ -channel* MM_API mm_channel(instance* i, uint64_t ident, uint8_t create); +MM_API channel* mm_channel(instance* i, uint64_t ident, uint8_t create); //TODO channel* mm_channel_find() /* @@ -249,26 +242,26 @@ channel* MM_API mm_channel(instance* i, uint64_t ident, uint8_t create); * to be selected on. The backend will be notified when the descriptor * becomes ready to read via its registered mmbackend_process_fd call. */ -int MM_API mm_manage_fd(int fd, char* backend, int manage, void* impl); +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. */ -int MM_API mm_channel_event(channel* c, channel_value v); +MM_API int mm_channel_event(channel* c, channel_value v); /* * Query all active instances for a given backend. * *i will need to be freed by the caller. */ -int MM_API mm_backend_instances(char* backend, size_t* n, instance*** i); +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. */ -uint64_t MM_API mm_timestamp(); +MM_API uint64_t mm_timestamp(); /* * Create a channel-to-channel mapping. This API should not -- cgit v1.2.3 From 7c20eeea5b1ee8c3c93e29f92e907a14498f9b73 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 7 Aug 2019 22:59:02 +0200 Subject: Don't build the maweb backend yet --- backends/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/Makefile b/backends/Makefile index 3308ef0..2635ddc 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 -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o -- cgit v1.2.3 From cf93d280af47aea1bf8bdafa30eabb2c2de005b8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 10 Aug 2019 15:30:48 +0200 Subject: Implement stream client connections in libmmbackend (Fixes #19) --- backends/artnet.c | 2 +- backends/libmmbackend.c | 62 +++++++++++++++++++++++++++++++++++-------------- backends/libmmbackend.h | 22 ++++++++++++++---- backends/lua.md | 2 +- backends/osc.c | 2 +- backends/sacn.c | 2 +- 6 files changed, 67 insertions(+), 25 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index a6df4ab..7f3f08c 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -19,7 +19,7 @@ static int artnet_listener(char* host, char* port){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(fd < 0){ return -1; } diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index b27ebc5..2fd3b8b 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -52,7 +52,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){ +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast){ int fd = -1, status, yes = 1; struct addrinfo hints = { .ai_family = AF_UNSPEC, @@ -80,20 +80,31 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ fprintf(stderr, "Failed to enable SO_REUSEADDR on socket\n"); } - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to enable SO_BROADCAST on socket\n"); - } + if(mcast){ + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to enable SO_BROADCAST on socket\n"); + } - yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to disable IP_MULTICAST_LOOP on socket: %s\n", strerror(errno)); + yes = 0; + if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to disable IP_MULTICAST_LOOP on socket: %s\n", strerror(errno)); + } } - status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); - if(status < 0){ - close(fd); - continue; + if(listener){ + status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } + } + else{ + status = connect(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } } break; @@ -107,11 +118,11 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ //set nonblocking #ifdef _WIN32 - u_long mode = 1; - if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){ - closesocket(fd); - return 1; - } + u_long mode = 1; + if(ioctlsocket(fd, FIONBIO, &mode) != NO_ERROR){ + closesocket(fd); + return 1; + } #else int flags = fcntl(fd, F_GETFL, 0); if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ @@ -123,3 +134,20 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener){ return fd; } + +int mmbackend_send(int fd, uint8_t* data, size_t length){ + ssize_t total = 0, sent; + while(total < length){ + sent = send(fd, data + total, length - total, 0); + if(sent < 0){ + fprintf(stderr, "Failed to send: %s\n", strerror(errno)); + return 1; + } + total += sent; + } + return 0; +} + +int mmbackend_send_str(int fd, char* data){ + return mmbackend_send(fd, (uint8_t*) data, strlen(data)); +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 77cad6a..31c4b96 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -16,7 +16,8 @@ #include #include "../portability.h" -/* Parse spec as host specification in the form +/* + * Parse spec as host specification in the form * host port * into its constituent parts. * Returns offsets into the original string and modifies it. @@ -25,13 +26,26 @@ */ void mmbackend_parse_hostspec(char* spec, char** host, char** port); -/* Parse a given host / port combination into a sockaddr_storage +/* + * Parse a given host / port combination into a sockaddr_storage * suitable for usage with connect / sendto * Returns 0 on success */ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len); -/* Create a socket of given type and mode for a bind / connect host. +/* + * 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); +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast); + +/* + * Send arbitrary data over multiple writes if necessary + * Returns 1 on failure, 0 on success. + */ +int mmbackend_send(int fd, uint8_t* data, size_t length); + +/* + * Wraps mmbackend_send for cstrings + */ +int mmbackend_send_str(int fd, char* data); diff --git a/backends/lua.md b/backends/lua.md index 1c67477..6ad5c2a 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -37,7 +37,7 @@ Input values range between 0.0 and 1.0, output values are clamped to the same ra #### Global configuration -The backend does not take any global configuration. +The `lua` backend does not take any global configuration. #### Instance configuration diff --git a/backends/osc.c b/backends/osc.c index 03e431f..77bbde4 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -525,7 +525,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ return 1; } - data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(data->fd < 0){ fprintf(stderr, "Failed to bind for instance %s\n", inst->name); return 1; diff --git a/backends/sacn.c b/backends/sacn.c index 6f7d1a5..2f418e5 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -60,7 +60,7 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); if(fd < 0){ return -1; } -- cgit v1.2.3 From 48bf96602023b2ead855f13477b6f5e26b663b45 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 10 Aug 2019 20:30:07 +0200 Subject: Clean up & check unions --- backends/artnet.c | 5 +++++ backends/evdev.c | 14 +++++--------- backends/evdev.h | 9 +++++++++ backends/midi.c | 14 +++++--------- backends/midi.h | 10 ++++++++++ backends/osc.c | 13 +++++-------- backends/osc.h | 8 ++++++++ backends/sacn.c | 5 +++++ 8 files changed, 52 insertions(+), 26 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index 7f3f08c..8e47d4f 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -54,6 +54,11 @@ int init(){ .shutdown = artnet_shutdown }; + if(sizeof(artnet_instance_id) != sizeof(uint64_t)){ + fprintf(stderr, "ArtNet instance identification union out of bounds\n"); + return 1; + } + //register backend if(mm_backend_register(artnet)){ fprintf(stderr, "Failed to register ArtNet backend\n"); diff --git a/backends/evdev.c b/backends/evdev.c index 7a7913d..bd2098d 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -18,15 +18,6 @@ #define BACKEND_NAME "evdev" -typedef union { - struct { - uint32_t pad; - uint16_t type; - uint16_t code; - } fields; - uint64_t label; -} evdev_channel_ident; - static struct { uint8_t detect; } evdev_config = { @@ -46,6 +37,11 @@ int init(){ .shutdown = evdev_shutdown }; + if(sizeof(evdev_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "evdev channel identification union out of bounds\n"); + return 1; + } + if(mm_backend_register(evdev)){ fprintf(stderr, "Failed to register evdev backend\n"); return 1; diff --git a/backends/evdev.h b/backends/evdev.h index f89e362..b26664b 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -44,3 +44,12 @@ typedef struct /*_evdev_instance_model*/ { struct libevdev_uinput* output_ev; #endif } evdev_instance_data; + +typedef union { + struct { + uint32_t pad; + uint16_t type; + uint16_t code; + } fields; + uint64_t label; +} evdev_channel_ident; \ No newline at end of file diff --git a/backends/midi.c b/backends/midi.c index 9c6ba80..5b8e561 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -4,15 +4,6 @@ #define BACKEND_NAME "midi" static snd_seq_t* sequencer = NULL; -typedef union { - struct { - uint8_t pad[5]; - uint8_t type; - uint8_t channel; - uint8_t control; - } fields; - uint64_t label; -} midi_channel_ident; enum /*_midi_channel_type*/ { none = 0, @@ -44,6 +35,11 @@ int init(){ .shutdown = midi_shutdown }; + if(sizeof(midi_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "MIDI channel identification union out of bounds\n"); + return 1; + } + if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){ fprintf(stderr, "Failed to open ALSA sequencer\n"); return 1; diff --git a/backends/midi.h b/backends/midi.h index 556706f..5ec17ea 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -15,3 +15,13 @@ typedef struct /*_midi_instance_data*/ { char* read; char* write; } midi_instance_data; + +typedef union { + struct { + uint8_t pad[5]; + uint8_t type; + uint8_t channel; + uint8_t control; + } fields; + uint64_t label; +} midi_channel_ident; \ No newline at end of file diff --git a/backends/osc.c b/backends/osc.c index 77bbde4..beb5527 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -13,14 +13,6 @@ #define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4) #define BACKEND_NAME "osc" -typedef union { - struct { - uint32_t channel; - uint32_t parameter; - } fields; - uint64_t label; -} osc_channel_ident; - static struct { uint8_t detect; } osc_global_config = { @@ -40,6 +32,11 @@ int init(){ .shutdown = osc_shutdown }; + if(sizeof(osc_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "OSC channel identification union out of bounds\n"); + return 1; + } + //register backend if(mm_backend_register(osc)){ fprintf(stderr, "Failed to register OSC backend\n"); diff --git a/backends/osc.h b/backends/osc.h index b2aaea7..ab19463 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -66,3 +66,11 @@ typedef struct /*_osc_instance_data*/ { //peer fd int fd; } osc_instance_data; + +typedef union { + struct { + uint32_t channel; + uint32_t parameter; + } fields; + uint64_t label; +} osc_channel_ident; \ No newline at end of file diff --git a/backends/sacn.c b/backends/sacn.c index 2f418e5..470e3bd 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -44,6 +44,11 @@ int init(){ .shutdown = sacn_shutdown }; + if(sizeof(sacn_instance_id) != sizeof(uint64_t)){ + fprintf(stderr, "sACN instance identification union out of bounds\n"); + return 1; + } + //register the backend if(mm_backend_register(sacn)){ fprintf(stderr, "Failed to register sACN backend\n"); -- cgit v1.2.3 From bb6111986bf7a997055287b916d0822957c5d13c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 11 Aug 2019 20:29:17 +0200 Subject: Initial maweb backend --- README.md | 1 + TODO | 6 + backends/Makefile | 2 + backends/libmmbackend.c | 217 +++++++++++++++ backends/libmmbackend.h | 75 ++++++ backends/maweb.c | 695 ++++++++++++++++++++++++++++++++++++++++++++++++ backends/maweb.h | 69 +++++ backends/maweb.md | 142 ++++++++++ midimonster.c | 3 + monster.cfg | 29 +- 10 files changed, 1238 insertions(+), 1 deletion(-) create mode 100644 backends/maweb.c create mode 100644 backends/maweb.h create mode 100644 backends/maweb.md diff --git a/README.md b/README.md index 8a0d7f9..3e9bb88 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ support for the protocols to translate. * `liblua5.3-dev` (for the lua backend) * `libola-dev` (for the optional OLA backend) * `pkg-config` (as some projects and systems like to spread their files around) +* `libssl-dev` (for the MA Web Remote backend) * A C compiler * GNUmake diff --git a/TODO b/TODO index 5f4ce91..d04773b 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,12 @@ MIDI NRPN +keepalive channels per backend? +mm_backend_start might get some arguments so they don't have to fetch them all the time +mm_channel_resolver might get additional info about the mapping direction Note source in channel value struct Optimize core channel search (store backend offset) Printing backend / Verbose mode mm_managed_fd.impl is not freed currently + +rtpmidi mode=peer + mode=initiator diff --git a/backends/Makefile b/backends/Makefile index 2635ddc..582655c 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -33,8 +33,10 @@ sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: LDLIBS += -lws2_32 maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +maweb.so: LDLIBS = -lssl maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) maweb.dll: LDLIBS += -lws2_32 +maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 2fd3b8b..c98cfe3 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -151,3 +151,220 @@ int mmbackend_send(int fd, uint8_t* data, size_t length){ int mmbackend_send_str(int fd, char* data){ return mmbackend_send(fd, (uint8_t*) data, strlen(data)); } + +json_type json_identify(char* json, size_t length){ + size_t n; + + //skip leading blanks + for(n = 0; json[n] && n < length && isspace(json[n]); n++){ + } + + if(n == length){ + return JSON_INVALID; + } + + switch(json[n]){ + case '{': + return JSON_OBJECT; + case '[': + return JSON_ARRAY; + case '"': + return JSON_STRING; + case '-': + case '+': + return JSON_NUMBER; + default: + //true false null number + if(!strncmp(json + n, "true", 4) + || !strncmp(json + n, "false", 5)){ + return JSON_BOOL; + } + else if(!strncmp(json + n, "null", 4)){ + return JSON_NULL; + } + //a bit simplistic but it should do + if(isdigit(json[n])){ + return JSON_NUMBER; + } + } + return JSON_INVALID; +} + +size_t json_validate(char* json, size_t length){ + switch(json_identify(json, length)){ + case JSON_STRING: + return json_validate_string(json, length); + case JSON_ARRAY: + return json_validate_array(json, length); + case JSON_OBJECT: + return json_validate_object(json, length); + case JSON_INVALID: + return 0; + default: + return json_validate_value(json, length); + } +} + +size_t json_validate_string(char* json, size_t length){ + size_t string_length = 0, offset; + + for(offset = 0; json[offset] && offset < length && json[offset] != '"'; offset++){ + } + + if(offset == length){ + return 0; + } + + //find terminating quotation mark not preceded by escape + for(string_length = 1; offset + string_length < length + && isprint(json[offset + string_length]) + && (json[offset + string_length] != '"' || json[offset + string_length - 1] == '\\'); string_length++){ + } + + //complete string found + if(json[offset + string_length] == '"' && json[offset + string_length - 1] != '\\'){ + return offset + string_length + 1; + } + + return 0; +} + +size_t json_validate_array(char* json, size_t length){ + //TODO + return 0; +} + +size_t json_validate_object(char* json, size_t length){ + //TODO + return 0; +} + +size_t json_validate_value(char* json, size_t length){ + //TODO + return 0; +} + +size_t json_obj_offset(char* json, char* key){ + size_t offset = 0; + uint8_t match = 0; + + //skip whitespace + for(offset = 0; json[offset] && isspace(json[offset]); offset++){ + } + + if(json[offset] != '{'){ + return 0; + } + offset++; + + while(json_identify(json + offset, strlen(json + offset)) == JSON_STRING){ + //skip to key begin + for(; json[offset] && json[offset] != '"'; offset++){ + } + + if(!strncmp(json + offset + 1, key, strlen(key)) && json[offset + 1 + strlen(key)] == '"'){ + //key found + match = 1; + } + + offset += json_validate_string(json + offset, strlen(json + offset)); + + //skip to value separator + for(; json[offset] && json[offset] != ':'; offset++){ + } + + //skip whitespace + for(offset++; json[offset] && isspace(json[offset]); offset++){ + } + + if(match){ + return offset; + } + + //add length of value + offset += json_validate(json + offset, strlen(json + offset)); + + //find comma or closing brace + for(; json[offset] && json[offset] != ',' && json[offset] != '}'; offset++){ + } + + if(json[offset] == ','){ + offset++; + } + } + + return 0; +} + +json_type json_obj(char* json, char* key){ + size_t offset = json_obj_offset(json, key); + if(offset){ + return json_identify(json + offset, strlen(json + offset)); + } + return JSON_INVALID; +} + +uint8_t json_obj_bool(char* json, char* key, uint8_t fallback){ + size_t offset = json_obj_offset(json, key); + if(offset){ + if(!strncmp(json + offset, "true", 4)){ + return 1; + } + if(!strncmp(json + offset, "false", 5)){ + return 0; + } + } + return fallback; +} + +int64_t json_obj_int(char* json, char* key, int64_t fallback){ + char* next_token = NULL; + int64_t result; + size_t offset = json_obj_offset(json, key); + if(offset){ + result = strtol(json + offset, &next_token, 10); + if(next_token != json + offset){ + return result; + } + } + return fallback; +} + +double json_obj_double(char* json, char* key, double fallback){ + char* next_token = NULL; + int64_t result; + size_t offset = json_obj_offset(json, key); + if(offset){ + result = strtod(json + offset, &next_token); + if(next_token != json + offset){ + return result; + } + } + return fallback; +} + +char* json_obj_str(char* json, char* key, size_t* length){ + size_t offset = json_obj_offset(json, key), raw_length; + if(offset){ + raw_length = json_validate_string(json + offset, strlen(json + offset)); + if(length){ + *length = raw_length - 2; + } + return json + offset + 1; + } + return NULL; +} + +char* json_obj_strdup(char* json, char* key){ + size_t offset = json_obj_offset(json, key), raw_length; + char* rv = NULL; + if(offset){ + raw_length = json_validate_string(json + offset, strlen(json + offset)); + rv = calloc(raw_length - 1, sizeof(char)); + if(rv){ + memcpy(rv, json + offset + 1, raw_length - 2); + } + return rv; + } + return NULL; +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 31c4b96..aa0ac0c 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -16,6 +16,10 @@ #include #include "../portability.h" +/*** BACKEND IMPLEMENTATION LIBRARY ***/ + +/** Networking functions **/ + /* * Parse spec as host specification in the form * host port @@ -49,3 +53,74 @@ int mmbackend_send(int fd, uint8_t* data, size_t length); * Wraps mmbackend_send for cstrings */ int mmbackend_send_str(int fd, char* data); + + +/** JSON parsing **/ + +typedef enum /*_json_types*/ { + JSON_INVALID = 0, + JSON_STRING, + JSON_ARRAY, + JSON_OBJECT, + JSON_NUMBER, + JSON_BOOL, + JSON_NULL +} json_type; + +/* + * Try to identify the type of JSON data next in the buffer + * Will access at most the next `length` bytes + */ +json_type json_identify(char* json, size_t length); + +/* + * Validate that a buffer contains a valid JSON document/data within `length` bytes + * Returns the length of a detected JSON document, 0 otherwise (ie. parse failures) + */ +size_t json_validate(char* json, size_t length); + +size_t json_validate_string(char* json, size_t length); + +size_t json_validate_array(char* json, size_t length); + +size_t json_validate_object(char* json, size_t length); + +size_t json_validate_value(char* json, size_t length); + +/* + * Calculate offset for value of `key` + * Assumes a zero-terminated, validated JSON object as input + * Returns offset on success, 0 on failure + */ +size_t json_obj_offset(char* json, char* key); + +/* + * Check for for a key within a JSON object + * Assumes a zero-terminated, validated JSON object as input + * Returns type of value + */ +json_type json_obj(char* json, char* key); + +//json_type json_array(char* json, size_t index) + +/* + * Fetch boolean value for an object key + * Assumes a zero-terminated, validated JSON object as input + */ +uint8_t json_obj_bool(char* json, char* key, uint8_t fallback); + +/* + * Fetch integer/double value for an object key + * Assumes a zero-terminated validated JSON object as input + */ +int64_t json_obj_int(char* json, char* key, int64_t fallback); +double json_obj_double(char* json, char* key, double fallback); + +/* + * Fetch a string value for an object key + * Assumes a zero-terminated validated JSON object as input + * json_obj_strdup returns a newly-allocated buffer containing + * only the requested value + */ +char* json_obj_str(char* json, char* key, size_t* length); +char* json_obj_strdup(char* json, char* key); diff --git a/backends/maweb.c b/backends/maweb.c new file mode 100644 index 0000000..be4c2ac --- /dev/null +++ b/backends/maweb.c @@ -0,0 +1,695 @@ +#include +#include +#include +#ifndef MAWEB_NO_LIBSSL +#include +#endif + +#include "libmmbackend.h" +#include "maweb.h" + +#define BACKEND_NAME "maweb" +#define WS_LEN(a) ((a) & 0x7F) +#define WS_OP(a) ((a) & 0x0F) +#define WS_FLAG_FIN 0x80 +#define WS_FLAG_MASK 0x80 + +static uint64_t last_keepalive = 0; + +static char* cmdline_keys[] = { + "SET", + "PREV", + "NEXT", + "CLEAR", + "FIXTURE_CHANNEL", + "FIXTURE_GROUP_PRESET", + "EXEC_CUE", + "STORE_UPDATE", + "OOPS", + "ESC", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "PUNKT", + "PLUS", + "MINUS", + "THRU", + "IF", + "AT", + "FULL", + "HIGH", + "ENTER", + "OFF", + "ON", + "ASSIGN", + "LABEL", + "COPY", + "TIME", + "PAGE", + "MACRO", + "DELETE", + "GOTO", + "GO_PLUS", + "GO_MINUS", + "PAUSE", + "SELECT", + "FIXTURE", + "SEQU", + "CUE", + "PRESET", + "EDIT", + "UPDATE", + "EXEC", + "STORE", + "GROUP", + "PROG_ONLY", + "SPECIAL_DIALOGUE", + "SOLO", + "ODD", + "EVEN", + "WINGS", + "RESET", + "MA", + "layerMode", + "featureSort", + "fixtureSort", + "channelSort", + "hideName" +}; + +int init(){ + backend maweb = { + .name = BACKEND_NAME, + .conf = maweb_configure, + .create = maweb_instance, + .conf_instance = maweb_configure_instance, + .channel = maweb_channel, + .handle = maweb_set, + .process = maweb_handle, + .start = maweb_start, + .shutdown = maweb_shutdown + }; + + if(sizeof(maweb_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "maweb channel identification union out of bounds\n"); + return 1; + } + + //register backend + if(mm_backend_register(maweb)){ + fprintf(stderr, "Failed to register maweb backend\n"); + return 1; + } + return 0; +} + +static int maweb_configure(char* option, char* value){ + fprintf(stderr, "The maweb backend does not take any global configuration\n"); + return 1; +} + +static int maweb_configure_instance(instance* inst, char* option, char* value){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + char* host = NULL, *port = NULL; + #ifndef MAWEB_NO_LIBSSL + uint8_t password_hash[MD5_DIGEST_LENGTH]; + #endif + + if(!strcmp(option, "host")){ + mmbackend_parse_hostspec(value, &host, &port); + if(!host){ + fprintf(stderr, "Invalid host specified for maweb instance %s\n", inst->name); + return 1; + } + free(data->host); + data->host = strdup(host); + free(data->port); + data->port = NULL; + if(port){ + data->port = strdup(port); + } + return 0; + } + else if(!strcmp(option, "user")){ + free(data->user); + data->user = strdup(value); + return 0; + } + else if(!strcmp(option, "password")){ + #ifndef MAWEB_NO_LIBSSL + size_t n; + MD5((uint8_t*) value, strlen(value), (uint8_t*) password_hash); + data->pass = realloc(data->pass, (2 * MD5_DIGEST_LENGTH + 1) * sizeof(char)); + for(n = 0; n < MD5_DIGEST_LENGTH; n++){ + snprintf(data->pass + 2 * n, 3, "%02x", password_hash[n]); + } + return 0; + #else + fprintf(stderr, "This build of the maweb backend only supports the default password\n"); + return 1; + #endif + } + + fprintf(stderr, "Unknown configuration parameter %s for manet instance %s\n", option, inst->name); + return 1; +} + +static instance* maweb_instance(){ + instance* inst = mm_instance(); + if(!inst){ + return NULL; + } + + maweb_instance_data* data = calloc(1, sizeof(maweb_instance_data)); + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + data->fd = -1; + data->buffer = calloc(MAWEB_RECV_CHUNK, sizeof(uint8_t)); + if(!data->buffer){ + fprintf(stderr, "Failed to allocate memory\n"); + free(data); + return NULL; + } + data->allocated = MAWEB_RECV_CHUNK; + + inst->impl = data; + return inst; +} + +static channel* maweb_channel(instance* inst, char* spec){ + maweb_channel_ident ident = { + .label = 0 + }; + char* next_token = NULL; + size_t n; + + if(!strncmp(spec, "page", 4)){ + ident.fields.page = strtoul(spec + 4, &next_token, 10); + if(*next_token != '.'){ + fprintf(stderr, "Failed to parse maweb channel spec %s: Missing separator\n", spec); + return NULL; + } + + next_token++; + if(!strncmp(next_token, "fader", 5)){ + ident.fields.type = exec_fader; + next_token += 5; + } + else if(!strncmp(next_token, "upper", 5)){ + ident.fields.type = exec_upper; + next_token += 5; + } + else if(!strncmp(next_token, "lower", 5)){ + ident.fields.type = exec_lower; + next_token += 5; + } + else if(!strncmp(next_token, "flash", 5)){ + ident.fields.type = exec_flash; + next_token += 5; + } + else if(!strncmp(next_token, "button", 6)){ + ident.fields.type = exec_fader; + next_token += 6; + } + ident.fields.index = strtoul(next_token, NULL, 10); + } + else{ + for(n = 0; n < sizeof(cmdline_keys) / sizeof(char*); n++){ + if(!strcmp(spec, cmdline_keys[n])){ + ident.fields.type = cmdline_button; + ident.fields.index = n + 1; + ident.fields.page = 1; + break; + } + } + } + + if(ident.fields.type && ident.fields.index && ident.fields.page + && ident.fields.index <= 90){ + //actually, those are zero-indexed... + ident.fields.index--; + ident.fields.page--; + return mm_channel(inst, ident.label, 1); + } + fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec); + return NULL; +} + +static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload, size_t len){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + uint8_t frame_header[MAWEB_FRAME_HEADER_LENGTH] = ""; + size_t header_bytes = 2; + uint16_t* payload_len16 = (uint16_t*) (frame_header + 2); + uint64_t* payload_len64 = (uint64_t*) (frame_header + 2); + + frame_header[0] = WS_FLAG_FIN | op; + if(len <= 125){ + frame_header[1] = WS_FLAG_MASK | len; + } + else if(len <= 0xFFFF){ + frame_header[1] = WS_FLAG_MASK | 126; + *payload_len16 = htobe16(len); + header_bytes += 2; + } + else{ + frame_header[1] = WS_FLAG_MASK | 127; + *payload_len64 = htobe64(len); + header_bytes += 8; + } + //send a zero masking key because masking is stupid + header_bytes += 4; + + if(mmbackend_send(data->fd, frame_header, header_bytes) + || mmbackend_send(data->fd, payload, len)){ + return 1; + } + + return 0; +} + +static int maweb_handle_message(instance* inst, char* payload, size_t payload_length){ + char xmit_buffer[MAWEB_XMIT_CHUNK]; + char* field; + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + + fprintf(stderr, "maweb message (%lu): %s\n", payload_length, payload); + if(json_obj(payload, "session") == JSON_NUMBER){ + data->session = json_obj_int(payload, "session", data->session); + fprintf(stderr, "maweb session id is now %ld\n", data->session); + } + + if(json_obj_bool(payload, "forceLogin", 0)){ + fprintf(stderr, "maweb sending user credentials\n"); + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"login\",\"username\":\"%s\",\"password\":\"%s\",\"session\":%ld}", + data->user, data->pass, data->session); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + } + + if(json_obj(payload, "status") && json_obj(payload, "appType")){ + fprintf(stderr, "maweb connection established\n"); + maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); + } + + if(json_obj(payload, "responseType") == JSON_STRING){ + field = json_obj_str(payload, "responseType", NULL); + if(!strncmp(field, "login", 5)){ + if(json_obj_bool(payload, "result", 0)){ + fprintf(stderr, "maweb login successful\n"); + data->login = 1; + } + else{ + fprintf(stderr, "maweb login failed\n"); + data->login = 0; + } + } + else if(!strncmp(field, "getdata", 7)){ + //FIXME stupid keepalive logic + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"getdata\"," + "\"data\":\"set,clear,solo,high\"," + "\"realtime\":true," + "\"maxRequests\":10," + ",\"session\":%ld}", + data->session); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + } + } + + return 0; +} + +static int maweb_connect(instance* inst){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + if(!data->host){ + return 1; + } + + //unregister old fd from core + if(data->fd >= 0){ + mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); + } + + data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0); + if(data->fd < 0){ + return 1; + } + + data->state = ws_new; + if(mmbackend_send_str(data->fd, "GET /?ma=1 HTTP/1.1\r\n") + || mmbackend_send_str(data->fd, "Connection: Upgrade\r\n") + || mmbackend_send_str(data->fd, "Upgrade: websocket\r\n") + || mmbackend_send_str(data->fd, "Sec-WebSocket-Version: 13\r\n") + //the websocket key probably should not be hardcoded, but this is not security criticial + //and the whole websocket 'accept key' dance is plenty stupid as it is + || mmbackend_send_str(data->fd, "Sec-WebSocket-Key: rbEQrXMEvCm4ZUjkj6juBQ==\r\n") + || mmbackend_send_str(data->fd, "\r\n")){ + fprintf(stderr, "maweb backend failed to communicate with peer\n"); + return 1; + } + + //register new fd + if(mm_manage_fd(data->fd, BACKEND_NAME, 1, (void*) inst)){ + fprintf(stderr, "maweb backend failed to register fd\n"); + return 1; + } + return 0; +} + +static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + size_t n, begin = 0; + + for(n = 0; n < bytes_read - 2; n++){ + if(!strncmp((char*) data->buffer + data->offset + n, "\r\n", 2)){ + if(data->state == ws_new){ + if(!strncmp((char*) data->buffer, "HTTP/1.1 101", 12)){ + data->state = ws_http; + } + else{ + fprintf(stderr, "maweb received invalid HTTP response for instance %s\n", inst->name); + return -1; + } + } + else{ + //ignore all http stuff until the end of headers since we don't actually care... + if(n == begin){ + data->state = ws_open; + } + } + begin = n + 2; + } + } + + return begin; +} + +static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + size_t header_length = 2; + uint64_t payload_length = 0; + uint16_t* payload_len16 = (uint16_t*) (data->buffer + 2); + uint64_t* payload_len64 = (uint64_t*) (data->buffer + 2); + uint8_t* payload = data->buffer + 2; + uint8_t terminator_temp = 0; + + if(data->offset + bytes_read < 2){ + return 0; + } + + //using varint as payload length is stupid, but some people seem to think otherwise... + payload_length = WS_LEN(data->buffer[1]); + switch(payload_length){ + case 126: + if(data->offset + bytes_read < 4){ + return 0; + } + payload_length = htobe16(*payload_len16); + payload = data->buffer + 4; + header_length = 4; + break; + case 127: + if(data->offset + bytes_read < 10){ + return 0; + } + payload_length = htobe64(*payload_len64); + payload = data->buffer + 10; + header_length = 10; + break; + default: + break; + } + + if(data->offset + bytes_read < header_length + payload_length){ + return 0; + } + + switch(WS_OP(data->buffer[0])){ + case ws_text: + //terminate message + terminator_temp = payload[payload_length]; + payload[payload_length] = 0; + if(maweb_handle_message(inst, (char*) payload, payload_length)){ + return data->offset + bytes_read; + } + payload[payload_length] = terminator_temp; + break; + case ws_ping: + //answer server ping with a pong + if(maweb_send_frame(inst, ws_pong, payload, payload_length)){ + fprintf(stderr, "maweb failed to send pong\n"); + } + return header_length + payload_length; + default: + fprintf(stderr, "maweb encountered unhandled frame type %02X\n", WS_OP(data->buffer[0])); + //this is somewhat dicey, it might be better to handle only header + payload length for known but unhandled types + return data->offset + bytes_read; + } + + return header_length + payload_length; +} + +static int maweb_handle_fd(instance* inst){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + ssize_t bytes_read, bytes_left = data->allocated - data->offset, bytes_handled; + + if(bytes_left < 3){ + data->buffer = realloc(data->buffer, (data->allocated + MAWEB_RECV_CHUNK) * sizeof(uint8_t)); + if(!data->buffer){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + data->allocated += MAWEB_RECV_CHUNK; + bytes_left += MAWEB_RECV_CHUNK; + } + + bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0); + if(bytes_read < 0){ + fprintf(stderr, "maweb backend failed to receive: %s\n", strerror(errno)); + //TODO close, reopen + return 1; + } + else if(bytes_read == 0){ + //client closed connection + //TODO try to reopen + return 0; + } + + do{ + switch(data->state){ + case ws_new: + case ws_http: + bytes_handled = maweb_handle_lines(inst, bytes_read); + break; + case ws_open: + bytes_handled = maweb_handle_ws(inst, bytes_read); + break; + case ws_closed: + bytes_handled = data->offset + bytes_read; + break; + } + + if(bytes_handled < 0){ + bytes_handled = data->offset + bytes_read; + //TODO close, reopen + fprintf(stderr, "maweb failed to handle incoming data\n"); + return 1; + } + else if(bytes_handled == 0){ + break; + } + + memmove(data->buffer, data->buffer + bytes_handled, (data->offset + bytes_read) - bytes_handled); + + //FIXME this might be somewhat borked + bytes_read -= data->offset; + bytes_handled -= data->offset; + bytes_read -= bytes_handled; + data->offset = 0; + } while(bytes_read > 0); + + data->offset += bytes_read; + return 0; +} + +static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + char xmit_buffer[MAWEB_XMIT_CHUNK]; + maweb_channel_ident ident; + size_t n; + + if(num && !data->login){ + fprintf(stderr, "maweb instance %s can not send output, not logged in\n", inst->name); + } + + for(n = 0; n < num; n++){ + ident.label = c[n]->ident; + switch(ident.fields.type){ + case exec_fader: + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"playbacks_userInput\"," + "\"execIndex\":%d," + "\"pageIndex\":%d," + "\"faderValue\":%f," + "\"type\":1," + "\"session\":%ld" + "}", ident.fields.index, ident.fields.page, v[n].normalised, data->session); + fprintf(stderr, "maweb out %s\n", xmit_buffer); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + break; + case exec_upper: + case exec_lower: + case exec_flash: + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"playbacks_userInput\"," + //"\"cmdline\":\"\"," + "\"execIndex\":%d," + "\"pageIndex\":%d," + "\"buttonId\":%d," + "\"pressed\":%s," + "\"released\":%s," + "\"type\":0," + "\"session\":%ld" + "}", ident.fields.index, ident.fields.page, + (exec_flash - ident.fields.type), + (v[n].normalised > 0.9) ? "true" : "false", + (v[n].normalised > 0.9) ? "false" : "true", + data->session); + fprintf(stderr, "maweb out %s\n", xmit_buffer); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + break; + case cmdline_button: + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"keyname\":\"%s\"," + //"\"autoSubmit\":false," + "\"value\":%d" + "}", cmdline_keys[ident.fields.index], + (v[n].normalised > 0.9) ? 1 : 0); + fprintf(stderr, "maweb out %s\n", xmit_buffer); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + break; + default: + fprintf(stderr, "maweb control not yet implemented\n"); + break; + } + } + return 0; +} + +static int maweb_keepalive(){ + size_t n, u; + instance** inst = NULL; + maweb_instance_data* data = NULL; + char xmit_buffer[MAWEB_XMIT_CHUNK]; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + //send keep-alive messages for logged-in instances + for(u = 0; u < n; u++){ + data = (maweb_instance_data*) inst[u]->impl; + if(data->login){ + snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"session\":%ld}", data->session); + maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + } + } + + free(inst); + return 0; +} + +static int maweb_handle(size_t num, managed_fd* fds){ + size_t n = 0; + int rv = 0; + + for(n = 0; n < num; n++){ + rv |= maweb_handle_fd((instance*) fds[n].impl); + } + + if(last_keepalive && mm_timestamp() - last_keepalive >= MAWEB_CONNECTION_KEEPALIVE){ + rv |= maweb_keepalive(); + last_keepalive = mm_timestamp(); + } + + return rv; +} + +static int maweb_start(){ + size_t n, u; + instance** inst = NULL; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(u = 0; u < n; u++){ + if(maweb_connect(inst[u])){ + fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); + return 1; + } + } + + free(inst); + if(!n){ + return 0; + } + + fprintf(stderr, "maweb backend registering %lu descriptors to core\n", n); + + //initialize keepalive timeout + last_keepalive = mm_timestamp(); + return 0; +} + +static int maweb_shutdown(){ + size_t n, u; + instance** inst = NULL; + maweb_instance_data* data = NULL; + + //fetch all instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(u = 0; u < n; u++){ + data = (maweb_instance_data*) inst[u]->impl; + free(data->host); + data->host = NULL; + free(data->port); + data->port = NULL; + free(data->user); + data->user = NULL; + free(data->pass); + data->pass = NULL; + + close(data->fd); + data->fd = -1; + + free(data->buffer); + data->buffer = NULL; + + data->offset = data->allocated = 0; + data->state = ws_new; + } + + free(inst); + + fprintf(stderr, "maweb backend shut down\n"); + return 0; +} diff --git a/backends/maweb.h b/backends/maweb.h new file mode 100644 index 0000000..6e6e652 --- /dev/null +++ b/backends/maweb.h @@ -0,0 +1,69 @@ +#include "midimonster.h" + +int init(); +static int maweb_configure(char* option, char* value); +static int maweb_configure_instance(instance* inst, char* option, char* value); +static instance* maweb_instance(); +static channel* maweb_channel(instance* inst, char* spec); +static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v); +static int maweb_handle(size_t num, managed_fd* fds); +static int maweb_start(); +static int maweb_shutdown(); + +//Default login password: MD5("midimonster") +#define MAWEB_DEFAULT_PASSWORD "2807623134739142b119aff358f8a219" +#define MAWEB_DEFAULT_PORT "80" +#define MAWEB_RECV_CHUNK 1024 +#define MAWEB_XMIT_CHUNK 2048 +#define MAWEB_FRAME_HEADER_LENGTH 16 +#define MAWEB_CONNECTION_KEEPALIVE 10000 + +typedef enum /*_maweb_channel_type*/ { + type_unset = 0, + exec_fader = 1, + exec_button = 2, + exec_upper = 3, + exec_lower = 4, + exec_flash = 5, + cmdline_button +} maweb_channel_type; + +typedef enum /*_ws_conn_state*/ { + ws_new, + ws_http, + ws_open, + ws_closed +} maweb_state; + +typedef enum /*_ws_frame_op*/ { + ws_text = 1, + ws_binary = 2, + ws_ping = 9, + ws_pong = 10 +} maweb_operation; + +typedef union { + struct { + uint8_t padding[3]; + uint8_t type; + uint16_t page; + uint16_t index; + } fields; + uint64_t label; +} maweb_channel_ident; + +typedef struct /*_maweb_instance_data*/ { + char* host; + char* port; + char* user; + char* pass; + + uint8_t login; + int64_t session; + + int fd; + maweb_state state; + size_t offset; + size_t allocated; + uint8_t* buffer; +} maweb_instance_data; diff --git a/backends/maweb.md b/backends/maweb.md new file mode 100644 index 0000000..eb1ed44 --- /dev/null +++ b/backends/maweb.md @@ -0,0 +1,142 @@ +### The `maweb` backend + +This backend connects directly with the integrated *MA Web Remote* of MA Lighting consoles and OnPC +instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC). +It grants read-write access to the console's playback faders and buttons as well as write access to +the command line buttons. + +To allow this backend to connect to the console, enter the console configuration (`Setup` key), +select `Console`/`Global Settings` and set the `Remotes` option to `Login enabled`. +Create an additional user that is able to log into the Web Remote using `Setup`/`Console`/`User & Profiles Setup`. + +#### Global configuration + +The `maweb` backend does not take any global configuration. + +#### 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 | +| `password` | `midimonster` | `midimonster` | Password for the remote session | + +#### Channel specification + +Currently, three types of channels can be assigned + +##### Executors + +Executors are arranged in pages, with each page having 90 fader executors and 90 button executors. +Note that when creating a new show, only the first page is created and active. + +A fader executor consists of a fader, two buttons (`upper`, `lower`) above it and one `flash` button below it. + +These controls can be adressed like + +``` +mw1.page1.fader5 > mw1.page1.upper5 +mw1.page3.lower3 > mw1.page2.flash2 +``` + +A button executor can likewise be mapped using the syntax + +``` +mw1.page2.button3 > mw1.page3.fader1 +``` + +##### Command line buttons + +Command line buttons will be pressed when the incoming event value is greater than `0.9` and released when it is less than that. +They can be mapped using the syntax + +``` +mw1. +``` + +The following button names are recognized by the backend: + +* `SET` +* `PREV` +* `NEXT` +* `CLEAR` +* `FIXTURE_CHANNEL` +* `FIXTURE_GROUP_PRESET` +* `EXEC_CUE` +* `STORE_UPDATE` +* `OOPS` +* `ESC` +* `0` +* `1` +* `2` +* `3` +* `4` +* `5` +* `6` +* `7` +* `8` +* `9` +* `PUNKT` +* `PLUS` +* `MINUS` +* `THRU` +* `IF` +* `AT` +* `FULL` +* `HIGH` +* `ENTER` +* `OFF` +* `ON` +* `ASSIGN` +* `LABEL` +* `COPY` +* `TIME` +* `PAGE` +* `MACRO` +* `DELETE` +* `GOTO` +* `GO_PLUS` +* `GO_MINUS` +* `PAUSE` +* `SELECT` +* `FIXTURE` +* `SEQU` +* `CUE` +* `PRESET` +* `EDIT` +* `UPDATE` +* `EXEC` +* `STORE` +* `GROUP` +* `PROG_ONLY` +* `SPECIAL_DIALOGUE` +* `SOLO` +* `ODD` +* `EVEN` +* `WINGS` +* `RESET` +* `MA` +* `layerMode` +* `featureSort` +* `fixtureSort` +* `channelSort` +* `hideName` + +Note that each Web Remote connection has it's own command line, as such commands entered using this backend will not affect +the command line on the main console. To do that, you will need to use another backend to feed input to the MA, such as +the ArtNet or MIDI backends. + +#### Known bugs / problems + +To properly encode the user password, this backend depends on a library providing cryptographic functions (`libssl` / `openssl`). +Since this may be a problem on some platforms, the backend can be built with this requirement disabled, which also disables the possibility +to set arbitrary passwords. The backend will always try to log in with the default password `midimonster` in this case. The user name is still +configurable. + +This backend is currently in active development. It therefore has some limitations: + +* It outputs a lot of debug information +* It currently is write-only, channel events are only sent to the MA, not consumed by it +* Fader executors (and their buttons) seem to work, I haven't tested button executors yet. +* Command line events are sent, but I'm not sure they're being handled yet +* I have so far only tested it with GradMA2 OnPC diff --git a/midimonster.c b/midimonster.c index 1e47698..25cf4a0 100644 --- a/midimonster.c +++ b/midimonster.c @@ -298,6 +298,9 @@ int main(int argc, char** argv){ plugins_close(); return usage(argv[0]); } + + //load an initial timestamp + update_timestamp(); //start backends if(backends_start()){ diff --git a/monster.cfg b/monster.cfg index 2413f6d..d272cee 100644 --- a/monster.cfg +++ b/monster.cfg @@ -7,6 +7,33 @@ bind = 0.0.0.0 universe = 0 dest = 255.255.255.255 +[backend midi] +detect = on + +[backend evdev] +;detect = on + +[midi bcf] +read = BCF +write = BCF + +[evdev mouse] +input = TPPS +relaxis.REL_X = 255 +relaxis.REL_Y = -255 + +[maweb ma] +;host = 10.23.23.248 +host = 127.0.0.1 4040 +user = web +password = web + [map] +bcf.channel{0..7}.pitch > bcf.channel{0..7}.pitch +bcf.channel{0..7}.pitch > art.{1..8} -art.1+2 > loop.b +bcf.channel{0..7}.pitch > ma.page1.fader{1..8} +bcf.channel0.note{16..23} > ma.page1.upper{1..8} +bcf.channel0.note{24..31} > ma.page1.lower{1..8} +mouse.EV_REL.REL_Y > ma.page1.fader1 +mouse.EV_KEY.BTN_LEFT > ma.ASSIGN -- cgit v1.2.3 From cc3aa7d8c1d680e75374a0c296a34d66a919f201 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 11 Aug 2019 20:29:17 +0200 Subject: Minor text fix --- backends/maweb.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/maweb.md b/backends/maweb.md index eb1ed44..3a7372c 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -42,7 +42,7 @@ mw1.page3.lower3 > mw1.page2.flash2 A button executor can likewise be mapped using the syntax ``` -mw1.page2.button3 > mw1.page3.fader1 +mw1.page2.button3 > mw1.page3.button1 ``` ##### Command line buttons @@ -136,7 +136,7 @@ configurable. This backend is currently in active development. It therefore has some limitations: * It outputs a lot of debug information -* It currently is write-only, channel events are only sent to the MA, not consumed by it +* It currently is write-only, channel events are only sent to the MA, not generated by it * Fader executors (and their buttons) seem to work, I haven't tested button executors yet. * Command line events are sent, but I'm not sure they're being handled yet * I have so far only tested it with GradMA2 OnPC -- cgit v1.2.3 From bad0fdac1e725b4b2efbbbfff4cef74c2e05efb8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 12 Aug 2019 20:58:04 +0200 Subject: Fix maweb button execs --- backends/maweb.c | 27 ++++++++++++++++++++++++++- backends/maweb.md | 6 ++++-- monster.cfg | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index be4c2ac..38d3d69 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -218,10 +218,15 @@ static channel* maweb_channel(instance* inst, char* spec){ next_token += 5; } else if(!strncmp(next_token, "button", 6)){ - ident.fields.type = exec_fader; + ident.fields.type = exec_button; next_token += 6; } ident.fields.index = strtoul(next_token, NULL, 10); + + //fix up the identifiers for button execs + if(ident.fields.index > 100){ + ident.fields.index -= 100; + } } else{ for(n = 0; n < sizeof(cmdline_keys) / sizeof(char*); n++){ @@ -568,6 +573,26 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ fprintf(stderr, "maweb out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; + case exec_button: + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"requestType\":\"playbacks_userInput\"," + //"\"cmdline\":\"\"," + "\"execIndex\":%d," + "\"pageIndex\":%d," + "\"buttonId\":%d," + "\"pressed\":%s," + "\"released\":%s," + "\"type\":0," + "\"session\":%ld" + "}", ident.fields.index + 100, + ident.fields.page, + 0, + (v[n].normalised > 0.9) ? "true" : "false", + (v[n].normalised > 0.9) ? "false" : "true", + data->session); + fprintf(stderr, "maweb out %s\n", xmit_buffer); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + break; case cmdline_button: snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"keyname\":\"%s\"," diff --git a/backends/maweb.md b/backends/maweb.md index 3a7372c..096f69c 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -27,7 +27,9 @@ Currently, three types of channels can be assigned ##### Executors -Executors are arranged in pages, with each page having 90 fader executors and 90 button executors. +Executors are arranged in pages, with each page having 90 fader executors (numbered 1 through 90) and +90 button executors (numbered 101 through 190). + Note that when creating a new show, only the first page is created and active. A fader executor consists of a fader, two buttons (`upper`, `lower`) above it and one `flash` button below it. @@ -42,7 +44,7 @@ mw1.page3.lower3 > mw1.page2.flash2 A button executor can likewise be mapped using the syntax ``` -mw1.page2.button3 > mw1.page3.button1 +mw1.page2.button103 > mw1.page3.button101 ``` ##### Command line buttons diff --git a/monster.cfg b/monster.cfg index d272cee..e6258a7 100644 --- a/monster.cfg +++ b/monster.cfg @@ -36,4 +36,4 @@ bcf.channel{0..7}.pitch > ma.page1.fader{1..8} bcf.channel0.note{16..23} > ma.page1.upper{1..8} bcf.channel0.note{24..31} > ma.page1.lower{1..8} mouse.EV_REL.REL_Y > ma.page1.fader1 -mouse.EV_KEY.BTN_LEFT > ma.ASSIGN +mouse.EV_KEY.BTN_LEFT > ma.page2.button102 -- cgit v1.2.3 From 047c76b48d4dd72b68ffaf99a210f9a3d5460f71 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 12 Aug 2019 20:59:48 +0200 Subject: Placate spellintian... --- backends/maweb.c | 2 +- backends/maweb.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 38d3d69..b708015 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -355,7 +355,7 @@ static int maweb_connect(instance* inst){ || mmbackend_send_str(data->fd, "Connection: Upgrade\r\n") || mmbackend_send_str(data->fd, "Upgrade: websocket\r\n") || mmbackend_send_str(data->fd, "Sec-WebSocket-Version: 13\r\n") - //the websocket key probably should not be hardcoded, but this is not security criticial + //the websocket key probably should not be hardcoded, but this is not security critical //and the whole websocket 'accept key' dance is plenty stupid as it is || mmbackend_send_str(data->fd, "Sec-WebSocket-Key: rbEQrXMEvCm4ZUjkj6juBQ==\r\n") || mmbackend_send_str(data->fd, "\r\n")){ diff --git a/backends/maweb.md b/backends/maweb.md index 096f69c..b54f1c3 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -34,7 +34,7 @@ Note that when creating a new show, only the first page is created and active. A fader executor consists of a fader, two buttons (`upper`, `lower`) above it and one `flash` button below it. -These controls can be adressed like +These controls can be addressed like ``` mw1.page1.fader5 > mw1.page1.upper5 -- cgit v1.2.3 From c2bf894835d01c648e3f64826e69944a2a27373f Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 15 Aug 2019 10:55:23 +0200 Subject: Fix CI, add dot2 detection to maweb --- .travis.yml | 3 +++ backends/maweb.c | 7 +++++ backends/maweb.md | 81 +++++++++---------------------------------------------- 3 files changed, 23 insertions(+), 68 deletions(-) diff --git a/.travis.yml b/.travis.yml index 74af4c3..8d21ef3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -123,6 +123,9 @@ matrix: - os: linux dist: xenial env: TASK='spellintian-duplicates' + - os: linux + dist: xenial + env: TASK='codespell' env: global: diff --git a/backends/maweb.c b/backends/maweb.c index b708015..07fce12 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -303,6 +303,12 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le if(json_obj(payload, "status") && json_obj(payload, "appType")){ fprintf(stderr, "maweb connection established\n"); + field = json_obj_str(payload, "appType", NULL); + if(!strncmp(field, "dot2", 4)){ + fprintf(stderr, "maweb peer detected as dot2, forcing user name 'remote'\n"); + free(data->user); + data->user = strdup("remote"); + } maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); } @@ -535,6 +541,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ if(num && !data->login){ fprintf(stderr, "maweb instance %s can not send output, not logged in\n", inst->name); + return 0; } for(n = 0; n < num; n++){ diff --git a/backends/maweb.md b/backends/maweb.md index b54f1c3..dd13db9 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -15,10 +15,10 @@ The `maweb` backend does not take any global configuration. #### Instance configuration -| Option | Example value | Default value | Description | +| 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 | +| `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 | #### Channel specification @@ -58,71 +58,16 @@ mw1. The following button names are recognized by the backend: -* `SET` -* `PREV` -* `NEXT` -* `CLEAR` -* `FIXTURE_CHANNEL` -* `FIXTURE_GROUP_PRESET` -* `EXEC_CUE` -* `STORE_UPDATE` -* `OOPS` -* `ESC` -* `0` -* `1` -* `2` -* `3` -* `4` -* `5` -* `6` -* `7` -* `8` -* `9` -* `PUNKT` -* `PLUS` -* `MINUS` -* `THRU` -* `IF` -* `AT` -* `FULL` -* `HIGH` -* `ENTER` -* `OFF` -* `ON` -* `ASSIGN` -* `LABEL` -* `COPY` -* `TIME` -* `PAGE` -* `MACRO` -* `DELETE` -* `GOTO` -* `GO_PLUS` -* `GO_MINUS` -* `PAUSE` -* `SELECT` -* `FIXTURE` -* `SEQU` -* `CUE` -* `PRESET` -* `EDIT` -* `UPDATE` -* `EXEC` -* `STORE` -* `GROUP` -* `PROG_ONLY` -* `SPECIAL_DIALOGUE` -* `SOLO` -* `ODD` -* `EVEN` -* `WINGS` -* `RESET` -* `MA` -* `layerMode` -* `featureSort` -* `fixtureSort` -* `channelSort` -* `hideName` +| `SET` | `PREV` | `NEXT` | `CLEAR` | `FIXTURE_CHANNEL` | `FIXTURE_GROUP_PRESET` | `EXEC_CUE` | +| `STORE_UPDATE`| `OOPS` | `ESC` | `OFF` | `ON` | `MA` | `STORE` | +| `0` | `1` | `2` | `3` | `4` | `5` | `6` | +| `7` | `8` | `9` | `PUNKT` | `PLUS` | `MINUS` | `THRU` | +| `IF` | `AT` | `FULL` | `HIGH` | `ENTER` | `ASSIGN` | `LABEL` | +| `COPY` | `TIME` | `PAGE` | `MACRO` | `DELETE` | `GOTO` | `GO_PLUS` | +| `GO_MINUS` | `PAUSE` | `SELECT` | `FIXTURE` | `SEQU` | `CUE` | `PRESET` | +| `EDIT` | `UPDATE` | `EXEC` | `GROUP` | `PROG_ONLY` | `SPECIAL_DIALOGUE` | `SOLO` | +| `ODD` | `EVEN` | `WINGS` | `RESET` | `layerMode` | `featureSort` | `fixtureSort` | +| `channelSort` | `hideName` | | | | | | Note that each Web Remote connection has it's own command line, as such commands entered using this backend will not affect the command line on the main console. To do that, you will need to use another backend to feed input to the MA, such as -- cgit v1.2.3 From 7997c74b421437c64ad3c25d5e7943470a6b9bc7 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 16 Aug 2019 19:57:25 +0200 Subject: Implement dot2 specific workarounds --- backends/maweb.c | 16 ++++++++-------- backends/maweb.h | 10 +++++++++- backends/maweb.md | 35 +++++++++++++++++++++++++++-------- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 07fce12..4d93987 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -239,8 +239,7 @@ static channel* maweb_channel(instance* inst, char* spec){ } } - if(ident.fields.type && ident.fields.index && ident.fields.page - && ident.fields.index <= 90){ + if(ident.fields.type && ident.fields.index && ident.fields.page){ //actually, those are zero-indexed... ident.fields.index--; ident.fields.page--; @@ -297,7 +296,7 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le fprintf(stderr, "maweb sending user credentials\n"); snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"login\",\"username\":\"%s\",\"password\":\"%s\",\"session\":%ld}", - data->user, data->pass, data->session); + (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass, data->session); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } @@ -305,9 +304,10 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le fprintf(stderr, "maweb connection established\n"); field = json_obj_str(payload, "appType", NULL); if(!strncmp(field, "dot2", 4)){ - fprintf(stderr, "maweb peer detected as dot2, forcing user name 'remote'\n"); - free(data->user); - data->user = strdup("remote"); + data->peer_type = peer_dot2; + } + else if(!strncmp(field, "gma2", 4)){ + data->peer_type = peer_ma2; } maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); } @@ -573,7 +573,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"type\":0," "\"session\":%ld" "}", ident.fields.index, ident.fields.page, - (exec_flash - ident.fields.type), + (data->peer_type == peer_dot2) ? (ident.fields.type - 3) : (exec_flash - ident.fields.type), (v[n].normalised > 0.9) ? "true" : "false", (v[n].normalised > 0.9) ? "false" : "true", data->session); @@ -591,7 +591,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"released\":%s," "\"type\":0," "\"session\":%ld" - "}", ident.fields.index + 100, + "}", ident.fields.index, ident.fields.page, 0, (v[n].normalised > 0.9) ? "true" : "false", diff --git a/backends/maweb.h b/backends/maweb.h index 6e6e652..5f59cc1 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -28,6 +28,13 @@ typedef enum /*_maweb_channel_type*/ { cmdline_button } maweb_channel_type; +typedef enum /*_maweb_peer_type*/ { + peer_unidentified = 0, + peer_ma2, + peer_ma3, + peer_dot2 +} maweb_peer_type; + typedef enum /*_ws_conn_state*/ { ws_new, ws_http, @@ -57,9 +64,10 @@ typedef struct /*_maweb_instance_data*/ { char* port; char* user; char* pass; - + uint8_t login; int64_t session; + maweb_peer_type peer_type; int fd; maweb_state state; diff --git a/backends/maweb.md b/backends/maweb.md index dd13db9..6076f44 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -5,10 +5,15 @@ instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC). It grants read-write access to the console's playback faders and buttons as well as write access to the command line buttons. -To allow this backend to connect to the console, enter the console configuration (`Setup` key), -select `Console`/`Global Settings` and set the `Remotes` option to `Login enabled`. +#### Setting up the console + +For the GrandMA2 enter the console configuration (`Setup` key), select `Console`/`Global Settings` and +set the `Remotes` option to `Login enabled`. Create an additional user that is able to log into the Web Remote using `Setup`/`Console`/`User & Profiles Setup`. +For the dot2, enter the console configuration using the `Setup` key, select `Global Settings` and enable the +Web Remote. Set a web remote password using the option below the activation setting. + #### Global configuration The `maweb` backend does not take any global configuration. @@ -27,12 +32,23 @@ Currently, three types of channels can be assigned ##### Executors -Executors are arranged in pages, with each page having 90 fader executors (numbered 1 through 90) and -90 button executors (numbered 101 through 190). - -Note that when creating a new show, only the first page is created and active. - -A fader executor consists of a fader, two buttons (`upper`, `lower`) above it and one `flash` button below it. +* For the GrandMA2, executors are arranged in pages, with each page having 90 fader executors (numbered 1 through 90) + and 90 button executors (numbered 101 through 190). + * A fader executor consists of a `fader`, two buttons above it (`upper`, `lower`) and one `flash` button below it. + * A button executor consists of a `button` control. +* For the dot2, executors are also arranged in pages, but the controls are non-obviously numbered. + * For the faders, they are right-to-left from the Core Fader section (Faders 6 to 1) over the F-Wing 1 (Faders 13 to 6) to + F-Wing 2 (Faders 21 to 14). + * Above the fader sections are two rows of 21 `button` executors, numbered 122 through 101 (upper row) and 222 through 201 (lower row), + in the same order as the faders are. + * Fader executors have two buttons below them (`upper` and `lower`). + * The button executor section consists of six rows of 18 buttons, divided into two button wings. Buttons on the wings + are once again numbered right-to-left. + * B-Wing 1 has `button` executors 308 to 301 (top row), 408 to 401 (second row), and so on until 808 through 801 (bottom row) + * B-Wing 2 has 316 to 309 (top row) through 816 to 809 (bottom row) + +When creating a new show, only the first page is created and active. Additional pages have to be created explicitly within +the console before being usable. These controls can be addressed like @@ -45,6 +61,7 @@ A button executor can likewise be mapped using the syntax ``` mw1.page2.button103 > mw1.page3.button101 +mw1.page2.button803 > mw1.page3.button516 ``` ##### Command line buttons @@ -58,6 +75,8 @@ mw1. The following button names are recognized by the backend: +| Supported | Command | Line | Keys | | | | +|---------------|---------------|---------------|---------------|-----------------------|-------------------------------|---------------| | `SET` | `PREV` | `NEXT` | `CLEAR` | `FIXTURE_CHANNEL` | `FIXTURE_GROUP_PRESET` | `EXEC_CUE` | | `STORE_UPDATE`| `OOPS` | `ESC` | `OFF` | `ON` | `MA` | `STORE` | | `0` | `1` | `2` | `3` | `4` | `5` | `6` | -- cgit v1.2.3 From 25d168454d53bb990118825f9bca808876aa4d59 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 17 Aug 2019 00:01:43 +0200 Subject: Hopefully clarify text a bit --- backends/maweb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/maweb.md b/backends/maweb.md index 6076f44..d713d82 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -37,7 +37,7 @@ Currently, three types of channels can be assigned * A fader executor consists of a `fader`, two buttons above it (`upper`, `lower`) and one `flash` button below it. * A button executor consists of a `button` control. * For the dot2, executors are also arranged in pages, but the controls are non-obviously numbered. - * For the faders, they are right-to-left from the Core Fader section (Faders 6 to 1) over the F-Wing 1 (Faders 13 to 6) to + * For the faders, they are numerically right-to-left from the Core Fader section (Faders 6 to 1) over the F-Wing 1 (Faders 13 to 6) to F-Wing 2 (Faders 21 to 14). * Above the fader sections are two rows of 21 `button` executors, numbered 122 through 101 (upper row) and 222 through 201 (lower row), in the same order as the faders are. -- cgit v1.2.3 From 3adaad2a46cddaa4c79bda022c55f4eef7c3f5af Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 17 Aug 2019 00:04:51 +0200 Subject: Fix exec indexing --- backends/maweb.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 4d93987..79e223f 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -222,11 +222,6 @@ static channel* maweb_channel(instance* inst, char* spec){ next_token += 6; } ident.fields.index = strtoul(next_token, NULL, 10); - - //fix up the identifiers for button execs - if(ident.fields.index > 100){ - ident.fields.index -= 100; - } } else{ for(n = 0; n < sizeof(cmdline_keys) / sizeof(char*); n++){ -- cgit v1.2.3 From 4863711ee2dac065151f5c9ab5508eb479b25b72 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 17 Aug 2019 01:46:06 +0200 Subject: Implement evdev relative axis output (Fixes #20) --- README.md | 2 +- backends/evdev.c | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3e9bb88..ff6e9cb 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ on any other (or the same) supported protocol, for example to: * 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.conf)) -* Play games or type using MIDI controllers +* Play games, type, or control your mouse using MIDI controllers ([Example configuration](configs/midi-mouse.cfg)) [![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster) [![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) diff --git a/backends/evdev.c b/backends/evdev.c index bd2098d..b19cda8 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -445,7 +445,7 @@ static int evdev_start(){ static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) { #ifndef EVDEV_NO_UINPUT - size_t evt = 0; + size_t evt = 0, axis = 0; evdev_instance_data* data = (evdev_instance_data*) inst->impl; evdev_channel_ident ident = { .label = 0 @@ -467,7 +467,20 @@ static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) switch(ident.fields.type){ case EV_REL: - value = (v[evt].normalised < 0.5) ? -1 : ((v[evt].normalised > 0.5) ? 1 : 0); + for(axis = 0; axis < data->relative_axes; axis++){ + if(data->relative_axis[axis].code == ident.fields.code){ + value = (v[evt].normalised * data->relative_axis[axis].max) - data->relative_axis[axis].current; + data->relative_axis[axis].current = v[evt].normalised * data->relative_axis[axis].max; + + if(data->relative_axis[axis].inverted){ + value *= -1; + } + break; + } + } + if(axis == data->relative_axes){ + value = (v[evt].normalised < 0.5) ? -1 : ((v[evt].normalised > 0.5) ? 1 : 0); + } break; case EV_ABS: range = libevdev_get_abs_maximum(data->output_proto, ident.fields.code) - libevdev_get_abs_minimum(data->output_proto, ident.fields.code); -- cgit v1.2.3 From 33f4e9c7d865ce518b0ffaf49615121db52a6a9e Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 17 Aug 2019 01:46:36 +0200 Subject: Add new example config --- configs/midi-mouse.cfg | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 configs/midi-mouse.cfg diff --git a/configs/midi-mouse.cfg b/configs/midi-mouse.cfg new file mode 100644 index 0000000..7e110ff --- /dev/null +++ b/configs/midi-mouse.cfg @@ -0,0 +1,22 @@ +; Use a Launch Control MIDI controller as mouse input +; Running this configuration requires root privileges on most systems, +; as creating additional input devices could potentially be misused for +; nefarious purposes + +[backend midi] +detect = on + +[evdev mouse] +output = MIDI Mouse +relaxis.REL_X = 255 +relaxis.REL_Y = 255 + +[midi launch] +read = Launch + +[map] +launch.ch0.cc0 > mouse.EV_REL.REL_X +launch.ch0.cc1 > mouse.EV_REL.REL_Y + +launch.ch0.note0 > mouse.EV_KEY.BTN_LEFT +launch.ch0.note1 > mouse.EV_KEY.BTN_RIGHT -- cgit v1.2.3 From 7b3129b46a1011458744bd70279367f47979aef0 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 17 Aug 2019 01:47:48 +0200 Subject: Fix README link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff6e9cb..4d8fe18 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ on any other (or the same) supported protocol, for example to: * Dynamically route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller or run effects on TouchOSC (Flying faders demo [configuration](configs/flying-faders.cfg) and [script](configs/flying-faders.lua)) * 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.conf)) +* Control lighting fixtures or DAWs using gamepad controllers, trackballs, etc ([Example configuration](configs/evdev.cfg)) * Play games, type, or control your mouse using MIDI controllers ([Example configuration](configs/midi-mouse.cfg)) [![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster) [![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) -- cgit v1.2.3 From 62eabb1c1d840cbab26b9ea747b102550199bad3 Mon Sep 17 00:00:00 2001 From: Spacelord09 Date: Thu, 22 Aug 2019 20:02:54 +0200 Subject: Added a config to test the maweb backend. --- configs/maweb-flying faders.cfg | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 configs/maweb-flying faders.cfg diff --git a/configs/maweb-flying faders.cfg b/configs/maweb-flying faders.cfg new file mode 100644 index 0000000..0101f1e --- /dev/null +++ b/configs/maweb-flying faders.cfg @@ -0,0 +1,13 @@ +; Create a 'flying faders' effect using lua and output it onto maweb faders 1..6 + +[maweb ma] +host = 10.23.42.21 80 ; Thats the IP of your console or OnPC. +user = midimonster ; If a Dot2 is used, the username is automatically set to "remote". +password = midimonster + +[lua generator] +script = configs/flying-faders.lua + + +[map] +generator.wave{1..6} > ma.page1.fader{1..6} \ No newline at end of file -- cgit v1.2.3 From 5dcbae830db5289b4e269c1913511b890e3e1d5d Mon Sep 17 00:00:00 2001 From: Spacelord09 Date: Thu, 22 Aug 2019 20:12:21 +0200 Subject: Rename maweb-flying faders.cfg to maweb-flying-faders.cfg --- configs/maweb-flying faders.cfg | 13 ------------- configs/maweb-flying-faders.cfg | 13 +++++++++++++ 2 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 configs/maweb-flying faders.cfg create mode 100644 configs/maweb-flying-faders.cfg diff --git a/configs/maweb-flying faders.cfg b/configs/maweb-flying faders.cfg deleted file mode 100644 index 0101f1e..0000000 --- a/configs/maweb-flying faders.cfg +++ /dev/null @@ -1,13 +0,0 @@ -; Create a 'flying faders' effect using lua and output it onto maweb faders 1..6 - -[maweb ma] -host = 10.23.42.21 80 ; Thats the IP of your console or OnPC. -user = midimonster ; If a Dot2 is used, the username is automatically set to "remote". -password = midimonster - -[lua generator] -script = configs/flying-faders.lua - - -[map] -generator.wave{1..6} > ma.page1.fader{1..6} \ No newline at end of file diff --git a/configs/maweb-flying-faders.cfg b/configs/maweb-flying-faders.cfg new file mode 100644 index 0000000..85aeada --- /dev/null +++ b/configs/maweb-flying-faders.cfg @@ -0,0 +1,13 @@ +; Create a 'flying faders' effect using lua and output it onto maweb faders 1..6 + +[maweb ma] +host = 10.23.42.21 80 ; Thats the IP of your console or OnPC. +user = midimonster ; If a Dot2 is used, the username is automatically set to "remote". +password = midimonster + +[lua generator] +script = configs/flying-faders.lua + + +[map] +generator.wave{1..6} > ma.page1.fader{1..6} -- cgit v1.2.3 From 90759c9ff4fc4380eec03962656ae4b9ca09bf5a Mon Sep 17 00:00:00 2001 From: Spacelord09 Date: Thu, 22 Aug 2019 20:35:34 +0200 Subject: Added Dot2 B-wing to example config. --- configs/maweb-flying-faders.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/configs/maweb-flying-faders.cfg b/configs/maweb-flying-faders.cfg index 85aeada..e2fe6c6 100644 --- a/configs/maweb-flying-faders.cfg +++ b/configs/maweb-flying-faders.cfg @@ -10,4 +10,5 @@ script = configs/flying-faders.lua [map] -generator.wave{1..6} > ma.page1.fader{1..6} +generator.wave{1..6} > ma.page1.fader{1..6} ; Fader 1 to 6 +;generator.wave{7..14} > ma.page1.fader{7..14]} ; Fader 7 to 14 (F-wing1 on Dot2) -- cgit v1.2.3 From 8b016f61a4b3d3be0c7b1e311209ab991276af0c Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 22 Aug 2019 21:13:48 +0200 Subject: Implement input for the maweb backend (with a few limitations) --- README.md | 2 +- backends/Makefile | 4 +- backends/evdev.h | 3 +- backends/libmmbackend.c | 241 ++++++++++++++++++++++++++++++-- backends/libmmbackend.h | 33 ++--- backends/maweb.c | 358 ++++++++++++++++++++++++++++++++++++++++-------- backends/maweb.h | 14 +- backends/maweb.md | 36 +++-- backends/midi.h | 3 +- backends/osc.h | 3 +- 10 files changed, 582 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 4d8fe18..40f8fbb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ on any other (or the same) supported protocol, for example to: * Translate MIDI Control Changes into Notes ([Example configuration](configs/unifest-17.cfg)) * Translate MIDI Notes into ArtNet or sACN ([Example configuration](configs/launchctl-sacn.cfg)) * Translate OSC messages into MIDI ([Example configuration](configs/midi-osc.cfg)) -* Dynamically route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller or run effects on TouchOSC (Flying faders demo [configuration](configs/flying-faders.cfg) and [script](configs/flying-faders.lua)) +* Dynamically generate, route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) to create your own lighting controller or run effects on TouchOSC (Flying faders demo [configuration](configs/flying-faders.cfg) and [script](configs/flying-faders.lua)) * 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)) diff --git a/backends/Makefile b/backends/Makefile index 582655c..5c5b677 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 -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o diff --git a/backends/evdev.h b/backends/evdev.h index b26664b..48bd0ab 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -52,4 +52,5 @@ typedef union { uint16_t code; } fields; uint64_t label; -} evdev_channel_ident; \ No newline at end of file +} evdev_channel_ident; + diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index c98cfe3..ccbeb52 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -208,10 +208,11 @@ size_t json_validate(char* json, size_t length){ size_t json_validate_string(char* json, size_t length){ size_t string_length = 0, offset; - for(offset = 0; json[offset] && offset < length && json[offset] != '"'; offset++){ + //skip leading whitespace + for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){ } - if(offset == length){ + if(offset == length || json[offset] != '"'){ return 0; } @@ -230,17 +231,122 @@ size_t json_validate_string(char* json, size_t length){ } size_t json_validate_array(char* json, size_t length){ - //TODO + size_t offset = 0; + + //skip leading whitespace + for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){ + } + + if(offset == length || json[offset] != '['){ + return 0; + } + + for(offset++; offset < length; offset++){ + offset += json_validate(json + offset, length - offset); + + //skip trailing whitespace, find terminator + for(; offset < length && isspace(json[offset]); offset++){ + } + + if(json[offset] == ','){ + continue; + } + + if(json[offset] == ']'){ + return offset + 1; + } + + break; + } + return 0; } size_t json_validate_object(char* json, size_t length){ - //TODO + size_t offset = 0; + + //skip whitespace + for(offset = 0; json[offset] && isspace(json[offset]); offset++){ + } + + if(offset == length || json[offset] != '{'){ + return 0; + } + + for(offset++; offset < length; offset++){ + if(json_identify(json + offset, length - offset) != JSON_STRING){ + //still could be an empty object... + for(; offset < length && isspace(json[offset]); offset++){ + } + if(json[offset] == '}'){ + return offset + 1; + } + return 0; + } + offset += json_validate(json + offset, length - offset); + + //find value separator + for(; offset < length && isspace(json[offset]); offset++){ + } + + if(json[offset] != ':'){ + return 0; + } + + offset++; + offset += json_validate(json + offset, length - offset); + + //skip trailing whitespace + for(; json[offset] && isspace(json[offset]); offset++){ + } + + if(json[offset] == '}'){ + return offset + 1; + } + else if(json[offset] != ','){ + return 0; + } + } return 0; } size_t json_validate_value(char* json, size_t length){ - //TODO + size_t offset = 0, value_length; + + //skip leading whitespace + for(offset = 0; json[offset] && offset < length && isspace(json[offset]); offset++){ + } + + if(offset == length){ + return 0; + } + + //match complete values + if(length - offset >= 4 && !strncmp(json + offset, "null", 4)){ + return offset + 4; + } + else if(length - offset >= 4 && !strncmp(json + offset, "true", 4)){ + return offset + 4; + } + else if(length - offset >= 5 && !strncmp(json + offset, "false", 5)){ + return offset + 5; + } + + if(json[offset] == '-' || isdigit(json[offset])){ + //json number parsing is dumb. + for(value_length = 1; offset + value_length < length && + (isdigit(json[offset + value_length]) + || json[offset + value_length] == '+' + || json[offset + value_length] == '-' + || json[offset + value_length] == '.' + || tolower(json[offset + value_length]) == 'e'); value_length++){ + } + + if(value_length > 0){ + return offset + value_length; + } + } + return 0; } @@ -284,13 +390,51 @@ size_t json_obj_offset(char* json, char* key){ //add length of value offset += json_validate(json + offset, strlen(json + offset)); - //find comma or closing brace - for(; json[offset] && json[offset] != ',' && json[offset] != '}'; offset++){ + //skip trailing whitespace + for(; json[offset] && isspace(json[offset]); offset++){ } if(json[offset] == ','){ offset++; + continue; + } + + break; + } + + return 0; +} + +size_t json_array_offset(char* json, uint64_t key){ + size_t offset = 0, index = 0; + + //skip leading whitespace + for(offset = 0; json[offset] && isspace(json[offset]); offset++){ + } + + if(json[offset] != '['){ + return 0; + } + + for(offset++; index <= key; offset++){ + //skip whitespace + for(; json[offset] && isspace(json[offset]); offset++){ + } + + if(index == key){ + return offset; + } + + offset += json_validate(json + offset, strlen(json + offset)); + + //skip trailing whitespace, find terminator + for(; json[offset] && isspace(json[offset]); offset++){ } + + if(json[offset] != ','){ + break; + } + index++; } return 0; @@ -304,6 +448,14 @@ json_type json_obj(char* json, char* key){ return JSON_INVALID; } +json_type json_array(char* json, uint64_t key){ + size_t offset = json_array_offset(json, key); + if(offset){ + return json_identify(json + offset, strlen(json + offset)); + } + return JSON_INVALID; +} + uint8_t json_obj_bool(char* json, char* key, uint8_t fallback){ size_t offset = json_obj_offset(json, key); if(offset){ @@ -317,6 +469,19 @@ uint8_t json_obj_bool(char* json, char* key, uint8_t fallback){ return fallback; } +uint8_t json_array_bool(char* json, uint64_t key, uint8_t fallback){ + size_t offset = json_array_offset(json, key); + if(offset){ + if(!strncmp(json + offset, "true", 4)){ + return 1; + } + if(!strncmp(json + offset, "false", 5)){ + return 0; + } + } + return fallback; +} + int64_t json_obj_int(char* json, char* key, int64_t fallback){ char* next_token = NULL; int64_t result; @@ -332,7 +497,7 @@ int64_t json_obj_int(char* json, char* key, int64_t fallback){ double json_obj_double(char* json, char* key, double fallback){ char* next_token = NULL; - int64_t result; + double result; size_t offset = json_obj_offset(json, key); if(offset){ result = strtod(json + offset, &next_token); @@ -343,6 +508,32 @@ double json_obj_double(char* json, char* key, double fallback){ return fallback; } +int64_t json_array_int(char* json, uint64_t key, int64_t fallback){ + char* next_token = NULL; + int64_t result; + size_t offset = json_array_offset(json, key); + if(offset){ + result = strtol(json + offset, &next_token, 10); + if(next_token != json + offset){ + return result; + } + } + return fallback; +} + +double json_array_double(char* json, uint64_t key, double fallback){ + char* next_token = NULL; + double result; + size_t offset = json_array_offset(json, key); + if(offset){ + result = strtod(json + offset, &next_token); + if(next_token != json + offset){ + return result; + } + } + return fallback; +} + char* json_obj_str(char* json, char* key, size_t* length){ size_t offset = json_obj_offset(json, key), raw_length; if(offset){ @@ -356,15 +547,37 @@ char* json_obj_str(char* json, char* key, size_t* length){ } char* json_obj_strdup(char* json, char* key){ - size_t offset = json_obj_offset(json, key), raw_length; - char* rv = NULL; + size_t len = 0; + char* value = json_obj_str(json, key, &len), *rv = NULL; + if(len){ + rv = calloc(len + 1, sizeof(char)); + if(rv){ + memcpy(rv, value, len); + } + } + return rv; +} + +char* json_array_str(char* json, uint64_t key, size_t* length){ + size_t offset = json_array_offset(json, key), raw_length; if(offset){ raw_length = json_validate_string(json + offset, strlen(json + offset)); - rv = calloc(raw_length - 1, sizeof(char)); - if(rv){ - memcpy(rv, json + offset + 1, raw_length - 2); + if(length){ + *length = raw_length - 2; } - return rv; + return json + offset + 1; } return NULL; } + +char* json_array_strdup(char* json, uint64_t key){ + size_t len = 0; + char* value = json_array_str(json, key, &len), *rv = NULL; + if(len){ + rv = calloc(len + 1, sizeof(char)); + if(rv){ + memcpy(rv, value, len); + } + } + return rv; +} diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index aa0ac0c..5749119 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -78,49 +78,50 @@ json_type json_identify(char* json, size_t length); * Returns the length of a detected JSON document, 0 otherwise (ie. parse failures) */ size_t json_validate(char* json, size_t length); - size_t json_validate_string(char* json, size_t length); - size_t json_validate_array(char* json, size_t length); - size_t json_validate_object(char* json, size_t length); - size_t json_validate_value(char* json, size_t length); /* * Calculate offset for value of `key` - * Assumes a zero-terminated, validated JSON object as input + * Assumes a zero-terminated, validated JSON object / array as input * Returns offset on success, 0 on failure */ size_t json_obj_offset(char* json, char* key); +size_t json_array_offset(char* json, uint64_t key); /* - * Check for for a key within a JSON object - * Assumes a zero-terminated, validated JSON object as input + * Check for for a key within a JSON object / index within an array + * Assumes a zero-terminated, validated JSON object / array as input * Returns type of value */ json_type json_obj(char* json, char* key); - -//json_type json_array(char* json, size_t index) +json_type json_array(char* json, uint64_t key); /* - * Fetch boolean value for an object key - * Assumes a zero-terminated, validated JSON object as input + * Fetch boolean value for an object / array key + * Assumes a zero-terminated, validated JSON object / array as input */ uint8_t json_obj_bool(char* json, char* key, uint8_t fallback); +uint8_t json_array_bool(char* json, uint64_t key, uint8_t fallback); /* - * Fetch integer/double value for an object key - * Assumes a zero-terminated validated JSON object as input + * Fetch integer/double value for an object / array key + * Assumes a zero-terminated validated JSON object / array as input */ int64_t json_obj_int(char* json, char* key, int64_t fallback); double json_obj_double(char* json, char* key, double fallback); +int64_t json_array_int(char* json, uint64_t key, int64_t fallback); +double json_array_double(char* json, uint64_t key, double fallback); /* - * Fetch a string value for an object key - * Assumes a zero-terminated validated JSON object as input - * json_obj_strdup returns a newly-allocated buffer containing + * Fetch a string value for an object / array key + * Assumes a zero-terminated validated JSON object / array as input + * json_*_strdup returns a newly-allocated buffer containing * only the requested value */ char* json_obj_str(char* json, char* key, size_t* length); char* json_obj_strdup(char* json, char* key); +char* json_array_str(char* json, uint64_t key, size_t* length); +char* json_array_strdup(char* json, uint64_t key); diff --git a/backends/maweb.c b/backends/maweb.c index 79e223f..07595be 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -14,7 +14,13 @@ #define WS_FLAG_FIN 0x80 #define WS_FLAG_MASK 0x80 +//TODO test using different pages simultaneously +//TODO test dot2 button virtual faders in fader view + static uint64_t last_keepalive = 0; +static uint64_t update_interval = 50; +static uint64_t last_update = 0; +static uint64_t updates_inflight = 0; static char* cmdline_keys[] = { "SET", @@ -94,7 +100,8 @@ int init(){ .handle = maweb_set, .process = maweb_handle, .start = maweb_start, - .shutdown = maweb_shutdown + .shutdown = maweb_shutdown, + .interval = maweb_interval }; if(sizeof(maweb_channel_ident) != sizeof(uint64_t)){ @@ -110,8 +117,27 @@ int init(){ return 0; } +static int channel_comparator(const void* raw_a, const void* raw_b){ + maweb_channel_ident* a = (maweb_channel_ident*) raw_a; + maweb_channel_ident* b = (maweb_channel_ident*) raw_b; + + if(a->fields.page != b->fields.page){ + return a->fields.page - b->fields.page; + } + return a->fields.index - b->fields.index; +} + +static uint32_t maweb_interval(){ + return update_interval - (last_update % update_interval); +} + static int maweb_configure(char* option, char* value){ - fprintf(stderr, "The maweb backend does not take any global configuration\n"); + if(!strcmp(option, "interval")){ + update_interval = strtoul(value, NULL, 10); + return 0; + } + + fprintf(stderr, "Unknown maweb backend configuration option %s\n", option); return 1; } @@ -187,6 +213,7 @@ static instance* maweb_instance(){ } static channel* maweb_channel(instance* inst, char* spec){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; maweb_channel_ident ident = { .label = 0 }; @@ -214,7 +241,7 @@ static channel* maweb_channel(instance* inst, char* spec){ next_token += 5; } else if(!strncmp(next_token, "flash", 5)){ - ident.fields.type = exec_flash; + ident.fields.type = exec_button; next_token += 5; } else if(!strncmp(next_token, "button", 6)){ @@ -238,6 +265,24 @@ static channel* maweb_channel(instance* inst, char* spec){ //actually, those are zero-indexed... ident.fields.index--; ident.fields.page--; + + //check if the channel is already known + for(n = 0; n < data->input_channels; n++){ + if(data->input_channel[n].label == ident.label){ + break; + } + } + + if(n == data->input_channels){ + data->input_channel = realloc(data->input_channel, (data->input_channels + 1) * sizeof(maweb_channel_ident)); + if(!data->input_channel){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + data->input_channel[n].label = ident.label; + data->input_channels++; + } + return mm_channel(inst, ident.label, 1); } fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec); @@ -276,11 +321,216 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload return 0; } +static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){ + size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control; + channel* chan = NULL; + channel_value evt; + maweb_channel_ident ident = { + .fields.page = page, + .fields.index = json_obj_int(payload, "iExec", 191) + }; + + if(!exec_blocks){ + if(metatype == 3){ + //ignore unused buttons + return 0; + } + fprintf(stderr, "maweb missing exec block data on exec %d\n", ident.fields.index); + return 1; + } + + if(metatype == 3){ + exec_blocks += json_obj_offset(payload + exec_blocks, "items"); + } + + //TODO detect unused faders + //TODO state tracking for fader values / exec run state + + //iterate over executor blocks + for(offset = json_array_offset(payload + exec_blocks, block); offset; offset = json_array_offset(payload + exec_blocks, block)){ + control = exec_blocks + offset + json_obj_offset(payload + exec_blocks + offset, "fader"); + ident.fields.type = exec_fader; + chan = mm_channel(inst, ident.label, 0); + if(chan){ + evt.normalised = json_obj_double(payload + control, "v", 0.0); + mm_channel_event(chan, evt); + } + + ident.fields.type = exec_button; + chan = mm_channel(inst, ident.label, 0); + if(chan){ + evt.normalised = json_obj_int(payload, "isRun", 0); + mm_channel_event(chan, evt); + } + + //printf("maweb page %ld exec %d value %f running %lu\n", page, ident.fields.index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); + ident.fields.index++; + block++; + } + + return 0; +} + +static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, size_t payload_length){ + size_t base_offset = json_obj_offset(payload, "itemGroups"), group_offset, subgroup_offset, item_offset; + uint64_t group = 0, subgroup, item, metatype; + + if(!page){ + fprintf(stderr, "maweb received playbacks for invalid page\n"); + return 0; + } + + if(!base_offset){ + fprintf(stderr, "maweb playback data missing item key\n"); + return 0; + } + + //iterate .itemGroups + for(group_offset = json_array_offset(payload + base_offset, group); + group_offset; + group_offset = json_array_offset(payload + base_offset, group)){ + metatype = json_obj_int(payload + base_offset + group_offset, "itemsType", 0); + //iterate .itemGroups.items + //FIXME this is problematic if there is no "items" key + group_offset = group_offset + json_obj_offset(payload + base_offset + group_offset, "items"); + if(group_offset){ + subgroup = 0; + group_offset += base_offset; + for(subgroup_offset = json_array_offset(payload + group_offset, subgroup); + subgroup_offset; + subgroup_offset = json_array_offset(payload + group_offset, subgroup)){ + //iterate .itemGroups.items[n] + item = 0; + subgroup_offset += group_offset; + for(item_offset = json_array_offset(payload + subgroup_offset, item); + item_offset; + item_offset = json_array_offset(payload + subgroup_offset, item)){ + maweb_process_playback(inst, page, metatype, + payload + subgroup_offset + item_offset, + payload_length - subgroup_offset - item_offset); + item++; + } + subgroup++; + } + } + group++; + } + updates_inflight--; + fprintf(stderr, "maweb playback message processing done, %lu updates inflight\n", updates_inflight); + return 0; +} + +static int maweb_request_playbacks(instance* inst){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + char xmit_buffer[MAWEB_XMIT_CHUNK]; + int rv = 0; + + char item_indices[1024] = "[0,100,200]", item_counts[1024] = "[21,21,21]", item_types[1024] = "[2,3,3]"; + //char item_indices[1024] = "[300,400]", item_counts[1024] = "[18,18]", item_types[1024] = "[3,3]"; + size_t page_index = 0, view = 2, channel = 0, offsets[3], channel_offset, channels; + + if(updates_inflight){ + fprintf(stderr, "maweb skipping update request, %lu updates still inflight\n", updates_inflight); + return 0; + } + + for(channel = 0; channel < data->input_channels; channel++){ + offsets[0] = offsets[1] = offsets[2] = 0; + page_index = data->input_channel[channel].fields.page; + if(data->peer_type == peer_dot2){ + //TODO implement poll segmentation for dot + //"\"startIndex\":[0,100,200]," + //"\"itemsCount\":[21,21,21]," + //"\"itemsType\":[2,3,3]," + //"\"view\":2," + //view = (data->input_channel[channel].fields.index >= 300) ? 3 : 2; + //observed + //"startIndex":[300,400,500,600,700,800], + //"itemsCount":[13,13,13,13,13,13] + //"itemsType":[3,3,3,3,3,3] + /*fprintf(stderr, "range start at %lu.%lu (%lu/%lu) end at %lu.%lu (%lu/%lu)\n", + page_index, + data->input_channel[channel].fields.index, + channel, + data->input_channels, + page_index, + data->input_channel[channel + channel_offset - 1].fields.index, + channel + channel_offset - 1, + data->input_channels + );*/ + //only send one request currently + channel = data->input_channels; + } + else{ + view = (data->input_channel[channel].fields.index >= 100) ? 3 : 2; + //for the ma, the view equals the exec type + snprintf(item_types, sizeof(item_types), "[%lu]", view); + //this channel must be included, so it must be in range for the first startindex + snprintf(item_indices, sizeof(item_indices), "[%d]", (data->input_channel[channel].fields.index / 5) * 5); + + for(channel_offset = 1; channel + channel_offset < data->input_channels + && data->input_channel[channel].fields.page == data->input_channel[channel + channel_offset].fields.page + && data->input_channel[channel].fields.index / 100 == data->input_channel[channel + channel_offset].fields.index / 100; channel_offset++){ + } + + channels = data->input_channel[channel + channel_offset - 1].fields.index - (data->input_channel[channel].fields.index / 5) * 5; + + + snprintf(item_counts, sizeof(item_indices), "[%lu]", ((channels / 5) * 5 + 5)); + channel += channel_offset - 1; + } + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{" + "\"requestType\":\"playbacks\"," + "\"startIndex\":%s," + "\"itemsCount\":%s," + "\"pageIndex\":%lu," + "\"itemsType\":%s," + "\"view\":%lu," + "\"execButtonViewMode\":2," //extended + "\"buttonsViewMode\":0," //get vfader for button execs + "\"session\":%lu" + "}", + item_indices, + item_counts, + page_index, + item_types, + view, + data->session); + rv |= maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + //fprintf(stderr, "req: %s\n", xmit_buffer); + updates_inflight++; + } + + return rv; +} + static int maweb_handle_message(instance* inst, char* payload, size_t payload_length){ char xmit_buffer[MAWEB_XMIT_CHUNK]; char* field; maweb_instance_data* data = (maweb_instance_data*) inst->impl; + //query this early to save on unnecessary parser passes with stupid-huge data messages + if(json_obj(payload, "responseType") == JSON_STRING){ + field = json_obj_str(payload, "responseType", NULL); + if(!strncmp(field, "login", 5)){ + if(json_obj_bool(payload, "result", 0)){ + fprintf(stderr, "maweb login successful\n"); + data->login = 1; + } + else{ + fprintf(stderr, "maweb login failed\n"); + data->login = 0; + } + } + if(!strncmp(field, "playbacks", 9)){ + if(maweb_process_playbacks(inst, json_obj_int(payload, "iPage", 0), payload, payload_length)){ + fprintf(stderr, "maweb failed to handle/request input data\n"); + } + return 0; + } + } + fprintf(stderr, "maweb message (%lu): %s\n", payload_length, payload); if(json_obj(payload, "session") == JSON_NUMBER){ data->session = json_obj_int(payload, "session", data->session); @@ -294,7 +544,6 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass, data->session); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } - if(json_obj(payload, "status") && json_obj(payload, "appType")){ fprintf(stderr, "maweb connection established\n"); field = json_obj_str(payload, "appType", NULL); @@ -307,31 +556,6 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le maweb_send_frame(inst, ws_text, (uint8_t*) "{\"session\":0}", 13); } - if(json_obj(payload, "responseType") == JSON_STRING){ - field = json_obj_str(payload, "responseType", NULL); - if(!strncmp(field, "login", 5)){ - if(json_obj_bool(payload, "result", 0)){ - fprintf(stderr, "maweb login successful\n"); - data->login = 1; - } - else{ - fprintf(stderr, "maweb login failed\n"); - data->login = 0; - } - } - else if(!strncmp(field, "getdata", 7)){ - //FIXME stupid keepalive logic - snprintf(xmit_buffer, sizeof(xmit_buffer), - "{\"requestType\":\"getdata\"," - "\"data\":\"set,clear,solo,high\"," - "\"realtime\":true," - "\"maxRequests\":10," - ",\"session\":%ld}", - data->session); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); - } - } - return 0; } @@ -376,7 +600,7 @@ static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; size_t n, begin = 0; - for(n = 0; n < bytes_read - 2; n++){ + for(n = 0; n < bytes_read - 1; n++){ if(!strncmp((char*) data->buffer + data->offset + n, "\r\n", 2)){ if(data->state == ws_new){ if(!strncmp((char*) data->buffer, "HTTP/1.1 101", 12)){ @@ -397,7 +621,7 @@ static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ } } - return begin; + return data->offset + begin; } static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ @@ -507,6 +731,7 @@ static int maweb_handle_fd(instance* inst){ if(bytes_handled < 0){ bytes_handled = data->offset + bytes_read; + data->offset = 0; //TODO close, reopen fprintf(stderr, "maweb failed to handle incoming data\n"); return 1; @@ -517,8 +742,6 @@ static int maweb_handle_fd(instance* inst){ memmove(data->buffer, data->buffer + bytes_handled, (data->offset + bytes_read) - bytes_handled); - //FIXME this might be somewhat borked - bytes_read -= data->offset; bytes_handled -= data->offset; bytes_read -= bytes_handled; data->offset = 0; @@ -551,30 +774,10 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"type\":1," "\"session\":%ld" "}", ident.fields.index, ident.fields.page, v[n].normalised, data->session); - fprintf(stderr, "maweb out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; case exec_upper: case exec_lower: - case exec_flash: - snprintf(xmit_buffer, sizeof(xmit_buffer), - "{\"requestType\":\"playbacks_userInput\"," - //"\"cmdline\":\"\"," - "\"execIndex\":%d," - "\"pageIndex\":%d," - "\"buttonId\":%d," - "\"pressed\":%s," - "\"released\":%s," - "\"type\":0," - "\"session\":%ld" - "}", ident.fields.index, ident.fields.page, - (data->peer_type == peer_dot2) ? (ident.fields.type - 3) : (exec_flash - ident.fields.type), - (v[n].normalised > 0.9) ? "true" : "false", - (v[n].normalised > 0.9) ? "false" : "true", - data->session); - fprintf(stderr, "maweb out %s\n", xmit_buffer); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); - break; case exec_button: snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"playbacks_userInput\"," @@ -586,13 +789,11 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"released\":%s," "\"type\":0," "\"session\":%ld" - "}", ident.fields.index, - ident.fields.page, - 0, + "}", ident.fields.index, ident.fields.page, + (data->peer_type == peer_dot2 && ident.fields.type == exec_upper) ? 0 : (ident.fields.type - exec_button), (v[n].normalised > 0.9) ? "true" : "false", (v[n].normalised > 0.9) ? "false" : "true", data->session); - fprintf(stderr, "maweb out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; case cmdline_button: @@ -602,7 +803,6 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"value\":%d" "}", cmdline_keys[ident.fields.index], (v[n].normalised > 0.9) ? 1 : 0); - fprintf(stderr, "maweb out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; default: @@ -638,6 +838,29 @@ static int maweb_keepalive(){ return 0; } +static int maweb_poll(){ + size_t n, u; + instance** inst = NULL; + maweb_instance_data* data = NULL; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + //send data polls for logged-in instances + for(u = 0; u < n; u++){ + data = (maweb_instance_data*) inst[u]->impl; + if(data->login){ + maweb_request_playbacks(inst[u]); + } + } + + free(inst); + return 0; +} + static int maweb_handle(size_t num, managed_fd* fds){ size_t n = 0; int rv = 0; @@ -646,17 +869,24 @@ static int maweb_handle(size_t num, managed_fd* fds){ rv |= maweb_handle_fd((instance*) fds[n].impl); } + //FIXME all keepalive processing allocates temporary buffers, this might an optimization target if(last_keepalive && mm_timestamp() - last_keepalive >= MAWEB_CONNECTION_KEEPALIVE){ rv |= maweb_keepalive(); last_keepalive = mm_timestamp(); } + if(last_update && mm_timestamp() - last_update >= update_interval){ + rv |= maweb_poll(); + last_update = mm_timestamp(); + } + return rv; } static int maweb_start(){ size_t n, u; instance** inst = NULL; + maweb_instance_data* data = NULL; //fetch all defined instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ @@ -665,6 +895,10 @@ static int maweb_start(){ } for(u = 0; u < n; u++){ + //sort channels + data = (maweb_instance_data*) inst[u]->impl; + qsort(data->input_channel, data->input_channels, sizeof(maweb_channel_ident), channel_comparator); + if(maweb_connect(inst[u])){ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); return 1; @@ -678,8 +912,8 @@ static int maweb_start(){ fprintf(stderr, "maweb backend registering %lu descriptors to core\n", n); - //initialize keepalive timeout - last_keepalive = mm_timestamp(); + //initialize timeouts + last_keepalive = last_update = mm_timestamp(); return 0; } @@ -713,6 +947,10 @@ static int maweb_shutdown(){ data->offset = data->allocated = 0; data->state = ws_new; + + free(data->input_channel); + data->input_channel = NULL; + data->input_channels = 0; } free(inst); diff --git a/backends/maweb.h b/backends/maweb.h index 5f59cc1..a868426 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -9,22 +9,22 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v); static int maweb_handle(size_t num, managed_fd* fds); static int maweb_start(); static int maweb_shutdown(); +static uint32_t maweb_interval(); //Default login password: MD5("midimonster") #define MAWEB_DEFAULT_PASSWORD "2807623134739142b119aff358f8a219" #define MAWEB_DEFAULT_PORT "80" #define MAWEB_RECV_CHUNK 1024 -#define MAWEB_XMIT_CHUNK 2048 +#define MAWEB_XMIT_CHUNK 4096 #define MAWEB_FRAME_HEADER_LENGTH 16 #define MAWEB_CONNECTION_KEEPALIVE 10000 typedef enum /*_maweb_channel_type*/ { type_unset = 0, exec_fader = 1, - exec_button = 2, - exec_upper = 3, - exec_lower = 4, - exec_flash = 5, + exec_button = 2, //gma: 0 dot: 0 + exec_lower = 3, //gma: 1 dot: 1 + exec_upper = 4, //gma: 2 dot: 0 cmdline_button } maweb_channel_type; @@ -69,6 +69,10 @@ typedef struct /*_maweb_instance_data*/ { int64_t session; maweb_peer_type peer_type; + //need to keep an internal registry to optimize data polls + size_t input_channels; + maweb_channel_ident* input_channel; + int fd; maweb_state state; size_t offset; diff --git a/backends/maweb.md b/backends/maweb.md index d713d82..fe430db 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -2,8 +2,7 @@ This backend connects directly with the integrated *MA Web Remote* of MA Lighting consoles and OnPC instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC). -It grants read-write access to the console's playback faders and buttons as well as write access to -the command line buttons. +It grants read-write access to the console's playback controls as well as write access to the command line. #### Setting up the console @@ -16,7 +15,9 @@ Web Remote. Set a web remote password using the option below the activation sett #### Global configuration -The `maweb` backend does not take any global configuration. +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|---------------------------------------------------------------| +| `interval` | `100` | `50` | Query interval for input data polling (in msec) | #### Instance configuration @@ -28,39 +29,44 @@ The `maweb` backend does not take any global configuration. #### Channel specification -Currently, three types of channels can be assigned +Currently, three types of MA controls can be assigned, with each having some subcontrols + +* Fader executor +* Button executor +* Command line buttons ##### Executors * For the GrandMA2, executors are arranged in pages, with each page having 90 fader executors (numbered 1 through 90) and 90 button executors (numbered 101 through 190). - * A fader executor consists of a `fader`, two buttons above it (`upper`, `lower`) and one `flash` button below it. - * A button executor consists of a `button` control. + * A fader executor consists of a `fader`, two buttons above it (`upper`, `lower`) and one `button` below it. + * A button executor consists of a `button` control and a virtual `fader` (visible on the console in the "Action Buttons" view). * For the dot2, executors are also arranged in pages, but the controls are non-obviously numbered. * For the faders, they are numerically right-to-left from the Core Fader section (Faders 6 to 1) over the F-Wing 1 (Faders 13 to 6) to F-Wing 2 (Faders 21 to 14). * Above the fader sections are two rows of 21 `button` executors, numbered 122 through 101 (upper row) and 222 through 201 (lower row), in the same order as the faders are. * Fader executors have two buttons below them (`upper` and `lower`). - * The button executor section consists of six rows of 18 buttons, divided into two button wings. Buttons on the wings + * The button executor section consists of six rows of 16 buttons, divided into two button wings. Buttons on the wings are once again numbered right-to-left. - * B-Wing 1 has `button` executors 308 to 301 (top row), 408 to 401 (second row), and so on until 808 through 801 (bottom row) + * B-Wing 1 has `button` controls 308 to 301 (top row), 408 to 401 (second row), and so on until 808 through 801 (bottom row) * B-Wing 2 has 316 to 309 (top row) through 816 to 809 (bottom row) When creating a new show, only the first page is created and active. Additional pages have to be created explicitly within -the console before being usable. +the console before being usable. `fader` controls, when mapped as outputs from the MA, output their value, `button` controls +output 1 when the corresponding executor is running, 0 otherwise. These controls can be addressed like ``` mw1.page1.fader5 > mw1.page1.upper5 -mw1.page3.lower3 > mw1.page2.flash2 +mw1.page3.lower3 > mw1.page2.button2 ``` A button executor can likewise be mapped using the syntax ``` -mw1.page2.button103 > mw1.page3.button101 +mw1.page2.button103 > mw1.page3.fader101 mw1.page2.button803 > mw1.page3.button516 ``` @@ -99,10 +105,12 @@ Since this may be a problem on some platforms, the backend can be built with thi to set arbitrary passwords. The backend will always try to log in with the default password `midimonster` in this case. The user name is still configurable. +Data input from the console is done by actively querying the state of all mapped controls, which is resource-intensive if done +at low latency. A lower input interval value will produce data with lower latency, at the cost of network & CPU usage. +Higher values will make the input "step" more, but will not consume as many CPU cycles and network bandwidth. + This backend is currently in active development. It therefore has some limitations: * It outputs a lot of debug information -* It currently is write-only, channel events are only sent to the MA, not generated by it -* Fader executors (and their buttons) seem to work, I haven't tested button executors yet. * Command line events are sent, but I'm not sure they're being handled yet -* I have so far only tested it with GradMA2 OnPC +* For the dot2, currently only the Core & F-Wings are supported for input from the console, not the B-Wings diff --git a/backends/midi.h b/backends/midi.h index 5ec17ea..6c3fcf9 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -24,4 +24,5 @@ typedef union { uint8_t control; } fields; uint64_t label; -} midi_channel_ident; \ No newline at end of file +} midi_channel_ident; + diff --git a/backends/osc.h b/backends/osc.h index ab19463..dd5afb0 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -73,4 +73,5 @@ typedef union { uint32_t parameter; } fields; uint64_t label; -} osc_channel_ident; \ No newline at end of file +} osc_channel_ident; + -- cgit v1.2.3 From 88f59354ef22c815ca2731a13ac87677c3571343 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 22 Aug 2019 21:21:59 +0200 Subject: Fix spelling, fix comments Inline-comments are not currently supported --- configs/maweb-flying-faders.cfg | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/configs/maweb-flying-faders.cfg b/configs/maweb-flying-faders.cfg index e2fe6c6..03b0939 100644 --- a/configs/maweb-flying-faders.cfg +++ b/configs/maweb-flying-faders.cfg @@ -1,14 +1,17 @@ ; Create a 'flying faders' effect using lua and output it onto maweb faders 1..6 [maweb ma] -host = 10.23.42.21 80 ; Thats the IP of your console or OnPC. -user = midimonster ; If a Dot2 is used, the username is automatically set to "remote". +; That's the IP of your console or OnPC. +host = 10.23.42.21 80 +; If a Dot2 is used, the username is automatically set to "remote". +user = midimonster password = midimonster [lua generator] script = configs/flying-faders.lua - [map] -generator.wave{1..6} > ma.page1.fader{1..6} ; Fader 1 to 6 -;generator.wave{7..14} > ma.page1.fader{7..14]} ; Fader 7 to 14 (F-wing1 on Dot2) +; Fader 1 to 6 (Core Wing) +generator.wave{1..6} > ma.page1.fader{1..6} +; Fader 7 to 14 (F-wing1 on Dot2) +;generator.wave{7..14} > ma.page1.fader{7..14]} -- cgit v1.2.3 From bfa4cbb012ce49f59d983d275da8837b50872c16 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 22 Aug 2019 21:38:06 +0200 Subject: Update travis dependencies --- .travis.yml | 3 ++- backends/maweb.c | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8d21ef3..305dc61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ addons: - libevdev-dev - libola-dev - liblua5.3-dev + - libssl-dev packages: &core_build_gpp_latest - *core_build - gcc-8 @@ -162,7 +163,7 @@ install: before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then PATH=/usr/local/opt/ccache/libexec:$PATH; fi # Use ccache on Mac too #Coverity doesn't work with g++ 5 or 6, so only upgrade to g++ 4.9 for that - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) -a "$CC" = "gcc" ]; then export CC="ccache gcc-8"; export CXX="ccache g++-8"; fi diff --git a/backends/maweb.c b/backends/maweb.c index 07595be..9ba04fa 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -475,7 +475,6 @@ static int maweb_request_playbacks(instance* inst){ channels = data->input_channel[channel + channel_offset - 1].fields.index - (data->input_channel[channel].fields.index / 5) * 5; - snprintf(item_counts, sizeof(item_indices), "[%lu]", ((channels / 5) * 5 + 5)); channel += channel_offset - 1; } -- cgit v1.2.3 From 6464a418f2bf13c8c04b10abf5ebdc4c95aae861 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 22 Aug 2019 22:28:25 +0200 Subject: Try to fix failing build by providing additional hints to MacOS --- .travis.yml | 3 +++ backends/maweb.md | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 305dc61..9f2034c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -164,6 +164,9 @@ install: before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl; fi +# OpenSSL is not a proper install due to some Apple bull, so provide additional locations via the environment... + - export CPPFLAGS="$CPPFLAGS -I/usr/local/opt/openssl/include" + - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl/lib" - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then PATH=/usr/local/opt/ccache/libexec:$PATH; fi # Use ccache on Mac too #Coverity doesn't work with g++ 5 or 6, so only upgrade to g++ 4.9 for that - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) -a "$CC" = "gcc" ]; then export CC="ccache gcc-8"; export CXX="ccache g++-8"; fi diff --git a/backends/maweb.md b/backends/maweb.md index fe430db..f3b40b6 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -53,8 +53,8 @@ Currently, three types of MA controls can be assigned, with each having some sub * B-Wing 2 has 316 to 309 (top row) through 816 to 809 (bottom row) When creating a new show, only the first page is created and active. Additional pages have to be created explicitly within -the console before being usable. `fader` controls, when mapped as outputs from the MA, output their value, `button` controls -output 1 when the corresponding executor is running, 0 otherwise. +the console before being usable. When mapped as outputs, `fader` controls output their value, `button` controls output 1 when the corresponding +executor is running, 0 otherwise. These controls can be addressed like -- cgit v1.2.3 From 93de82b8ccab8fdbeaa2b1847c75488a03340bcc Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 22 Aug 2019 22:41:28 +0200 Subject: Update travis config for openssl, using CFLAGS this time --- .travis.yml | 2 +- backends/lua.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9f2034c..864c628 100644 --- a/.travis.yml +++ b/.travis.yml @@ -165,7 +165,7 @@ before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl; fi # OpenSSL is not a proper install due to some Apple bull, so provide additional locations via the environment... - - export CPPFLAGS="$CPPFLAGS -I/usr/local/opt/openssl/include" + - export CFLAGS="$CFLAGS -I/usr/local/opt/openssl/include" - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl/lib" - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then PATH=/usr/local/opt/ccache/libexec:$PATH; fi # Use ccache on Mac too #Coverity doesn't work with g++ 5 or 6, so only upgrade to g++ 4.9 for that diff --git a/backends/lua.c b/backends/lua.c index ec02575..365cf3e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -384,11 +384,11 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ } static int lua_handle(size_t num, managed_fd* fds){ - uint8_t read_buffer[100]; uint64_t delta = timer_interval; size_t n; #ifdef MMBACKEND_LUA_TIMERFD + uint8_t read_buffer[100]; if(!num){ return 0; } -- cgit v1.2.3 From b4b27aa4a90899d5b025bbef11d444d4c47800d9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 23 Aug 2019 19:47:53 +0200 Subject: Finalize & publish maweb backend --- Makefile | 2 +- README.md | 2 ++ backends/maweb.c | 92 ++++++++++++++++++++++++++++++++----------------------- backends/maweb.md | 6 +--- 4 files changed, 58 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index 57fe089..2d88c49 100644 --- a/Makefile +++ b/Makefile @@ -56,5 +56,5 @@ run: valgrind --leak-check=full --show-leak-kinds=all ./midimonster sanitize: export CC = clang -sanitize: export CFLAGS = -g -Wall -Wpedantic -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer +sanitize: export CFLAGS += -g -Wall -Wpedantic -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer sanitize: midimonster backends diff --git a/README.md b/README.md index 40f8fbb..5c4233f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Currently, the MIDIMonster supports the following protocols: * OpenSoundControl (OSC) * evdev input devices (Linux) * Open Lighting Architecture (OLA) +* MA Lighting Web Remote with additional flexibility provided by a Lua scripting environment. @@ -121,6 +122,7 @@ special information. These documentation files are located in the `backends/` di * [`ola` backend documentation](backends/ola.md) * [`osc` backend documentation](backends/osc.md) * [`lua` backend documentation](backends/lua.md) +* [`maweb` backend documentation](backends/maweb.md) ## Building diff --git a/backends/maweb.c b/backends/maweb.c index 9ba04fa..4e6fad1 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -266,14 +266,17 @@ static channel* maweb_channel(instance* inst, char* spec){ ident.fields.index--; ident.fields.page--; - //check if the channel is already known + //check if the (exec/meta) channel is already known for(n = 0; n < data->input_channels; n++){ - if(data->input_channel[n].label == ident.label){ + if(data->input_channel[n].fields.page == ident.fields.page + && data->input_channel[n].fields.index == ident.fields.index){ break; } } - if(n == data->input_channels){ + //FIXME only register channels that are mapped as outputs + //only register exec channels for updates + if(n == data->input_channels && ident.fields.type != cmdline_button){ data->input_channel = realloc(data->input_channel, (data->input_channels + 1) * sizeof(maweb_channel_ident)); if(!data->input_channel){ fprintf(stderr, "Failed to allocate memory\n"); @@ -324,9 +327,9 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){ size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control; channel* chan = NULL; - channel_value evt; + channel_value evt; maweb_channel_ident ident = { - .fields.page = page, + .fields.page = page - 1, .fields.index = json_obj_int(payload, "iExec", 191) }; @@ -335,10 +338,11 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty //ignore unused buttons return 0; } - fprintf(stderr, "maweb missing exec block data on exec %d\n", ident.fields.index); + fprintf(stderr, "maweb missing exec block data on exec %ld.%d\n", page, ident.fields.index); return 1; } + //the bottomButtons key has an additional subentry if(metatype == 3){ exec_blocks += json_obj_offset(payload + exec_blocks, "items"); } @@ -363,7 +367,7 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty mm_channel_event(chan, evt); } - //printf("maweb page %ld exec %d value %f running %lu\n", page, ident.fields.index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); + DBGPF("maweb page %ld exec %d value %f running %lu\n", page, ident.fields.index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); ident.fields.index++; block++; } @@ -416,7 +420,7 @@ static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, group++; } updates_inflight--; - fprintf(stderr, "maweb playback message processing done, %lu updates inflight\n", updates_inflight); + DBGPF("maweb playback message processing done, %lu updates inflight\n", updates_inflight); return 0; } @@ -425,45 +429,53 @@ static int maweb_request_playbacks(instance* inst){ char xmit_buffer[MAWEB_XMIT_CHUNK]; int rv = 0; - char item_indices[1024] = "[0,100,200]", item_counts[1024] = "[21,21,21]", item_types[1024] = "[2,3,3]"; - //char item_indices[1024] = "[300,400]", item_counts[1024] = "[18,18]", item_types[1024] = "[3,3]"; - size_t page_index = 0, view = 2, channel = 0, offsets[3], channel_offset, channels; + char item_indices[1024] = "[300,400,500]", item_counts[1024] = "[16,16,16]", item_types[1024] = "[3,3,3]"; + size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels; if(updates_inflight){ fprintf(stderr, "maweb skipping update request, %lu updates still inflight\n", updates_inflight); return 0; } + //don't quote me on this whole segment for(channel = 0; channel < data->input_channels; channel++){ - offsets[0] = offsets[1] = offsets[2] = 0; + offsets[0] = offsets[1] = offsets[2] = 1; page_index = data->input_channel[channel].fields.page; if(data->peer_type == peer_dot2){ - //TODO implement poll segmentation for dot - //"\"startIndex\":[0,100,200]," - //"\"itemsCount\":[21,21,21]," - //"\"itemsType\":[2,3,3]," - //"\"view\":2," - //view = (data->input_channel[channel].fields.index >= 300) ? 3 : 2; - //observed - //"startIndex":[300,400,500,600,700,800], - //"itemsCount":[13,13,13,13,13,13] - //"itemsType":[3,3,3,3,3,3] - /*fprintf(stderr, "range start at %lu.%lu (%lu/%lu) end at %lu.%lu (%lu/%lu)\n", - page_index, - data->input_channel[channel].fields.index, - channel, - data->input_channels, - page_index, - data->input_channel[channel + channel_offset - 1].fields.index, - channel + channel_offset - 1, - data->input_channels - );*/ - //only send one request currently - channel = data->input_channels; + //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view + view = (data->input_channel[channel].fields.index >= 300) ? 3 : 2; + + for(channel_offset = 1; channel + channel_offset <= data->input_channels; channel_offset++){ + channels = channel + channel_offset - 1; + //find end for this exec block + for(; channel + channel_offset < data->input_channels; channel_offset++){ + if(data->input_channel[channel + channel_offset].fields.page != page_index + || (data->input_channel[channels].fields.index / 100) != (data->input_channel[channel + channel_offset].fields.index / 100)){ + break; + } + } + + //add request block for the exec block + offsets[0] += snprintf(item_indices + offsets[0], sizeof(item_indices) - offsets[0], "%d,", data->input_channel[channels].fields.index); + offsets[1] += snprintf(item_counts + offsets[1], sizeof(item_counts) - offsets[1], "%d,", data->input_channel[channel + channel_offset - 1].fields.index - data->input_channel[channels].fields.index + 1); + offsets[2] += snprintf(item_types + offsets[2], sizeof(item_types) - offsets[2], "%d,", (data->input_channel[channels].fields.index < 100) ? 2 : 3); + + //send on page boundary, metamode boundary, last channel + if(channel + channel_offset >= data->input_channels + || data->input_channel[channel + channel_offset].fields.page != page_index + || (data->input_channel[channel].fields.index < 300) != (data->input_channel[channel + channel_offset].fields.index < 300)){ + break; + } + } + + //terminate arrays (overwriting the last array separator) + offsets[0] += snprintf(item_indices + offsets[0] - 1, sizeof(item_indices) - offsets[0], "]"); + offsets[1] += snprintf(item_counts + offsets[1] - 1, sizeof(item_counts) - offsets[1], "]"); + offsets[2] += snprintf(item_types + offsets[2] - 1, sizeof(item_types) - offsets[2], "]"); } else{ + //for the ma, the view equals the exec type requested (we can query all button execs from button view, all fader execs from fader view) view = (data->input_channel[channel].fields.index >= 100) ? 3 : 2; - //for the ma, the view equals the exec type snprintf(item_types, sizeof(item_types), "[%lu]", view); //this channel must be included, so it must be in range for the first startindex snprintf(item_indices, sizeof(item_indices), "[%d]", (data->input_channel[channel].fields.index / 5) * 5); @@ -476,8 +488,12 @@ static int maweb_request_playbacks(instance* inst){ channels = data->input_channel[channel + channel_offset - 1].fields.index - (data->input_channel[channel].fields.index / 5) * 5; snprintf(item_counts, sizeof(item_indices), "[%lu]", ((channels / 5) * 5 + 5)); - channel += channel_offset - 1; } + + //advance base channel + channel += channel_offset - 1; + + //send current request snprintf(xmit_buffer, sizeof(xmit_buffer), "{" "\"requestType\":\"playbacks\"," @@ -497,7 +513,7 @@ static int maweb_request_playbacks(instance* inst){ view, data->session); rv |= maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); - //fprintf(stderr, "req: %s\n", xmit_buffer); + DBGPF("maweb poll request: %s\n", xmit_buffer); updates_inflight++; } @@ -530,7 +546,7 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le } } - fprintf(stderr, "maweb message (%lu): %s\n", payload_length, payload); + DBGPF("maweb message (%lu): %s\n", payload_length, payload); if(json_obj(payload, "session") == JSON_NUMBER){ data->session = json_obj_int(payload, "session", data->session); fprintf(stderr, "maweb session id is now %ld\n", data->session); diff --git a/backends/maweb.md b/backends/maweb.md index f3b40b6..9fe4790 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -109,8 +109,4 @@ Data input from the console is done by actively querying the state of all mapped at low latency. A lower input interval value will produce data with lower latency, at the cost of network & CPU usage. Higher values will make the input "step" more, but will not consume as many CPU cycles and network bandwidth. -This backend is currently in active development. It therefore has some limitations: - -* It outputs a lot of debug information -* Command line events are sent, but I'm not sure they're being handled yet -* For the dot2, currently only the Core & F-Wings are supported for input from the console, not the B-Wings +Command line events are sent, but I'm not sure they're being handled yet. -- cgit v1.2.3 From ee8776b114fc141355675a20e3d7e49ec5e6c4d8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 23 Aug 2019 20:01:33 +0200 Subject: Fix Coverity CID 347886 --- backends/maweb.c | 1 + 1 file changed, 1 insertion(+) diff --git a/backends/maweb.c b/backends/maweb.c index 4e6fad1..80ab579 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -916,6 +916,7 @@ static int maweb_start(){ if(maweb_connect(inst[u])){ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); + free(inst); return 1; } } -- cgit v1.2.3 From fe952470ca4bae4cff25a9090e4c1b05a76a7961 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 23 Aug 2019 23:40:16 +0200 Subject: Update sACN backend documentation (Fixes #22) --- backends/sacn.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/sacn.md b/backends/sacn.md index 3d245a4..434beeb 100644 --- a/backends/sacn.md +++ b/backends/sacn.md @@ -16,7 +16,7 @@ containing all write-enabled universes. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `universe` | `0` | none | Universe identifier | +| `universe` | `1` | none | Universe identifier between 1 and 63999 | | `interface` | `1` | `0` | The bound address to use for data input/output | | `priority` | `100` | none | The data priority to transmit for this instance. Setting this option enables the instance for output and includes it in the universe discovery report. | | `destination` | `10.2.2.2` | Universe multicast | Destination address for unicast output. If unset, the multicast destination for the specified universe is used. | @@ -55,4 +55,4 @@ To use multicast input, all networking hardware in the path must support the IGM The Linux kernel limits the number of multicast groups an interface may join to 20. An instance configured for input automatically joins the multicast group for its universe, unless configured in `unicast` mode. -This limit can be raised by changing the kernel option in `/proc/sys/net/ipv4/igmp_max_memberships`. \ No newline at end of file +This limit can be raised by changing the kernel option in `/proc/sys/net/ipv4/igmp_max_memberships`. -- cgit v1.2.3 From 0906c00d23be220928a656476c26a490998e826e Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 24 Aug 2019 20:19:24 +0200 Subject: Only connect to ALSA when instances are requested --- backends/midi.c | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index 5b8e561..88b841c 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -3,6 +3,7 @@ #include "midi.h" #define BACKEND_NAME "midi" +static char* sequencer_name = NULL; static snd_seq_t* sequencer = NULL; enum /*_midi_channel_type*/ { @@ -40,29 +41,19 @@ int init(){ return 1; } - if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){ - fprintf(stderr, "Failed to open ALSA sequencer\n"); - return 1; - } - //register backend if(mm_backend_register(midi)){ fprintf(stderr, "Failed to register MIDI backend\n"); return 1; } - snd_seq_nonblock(sequencer, 1); - - fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer)); return 0; } static int midi_configure(char* option, char* value){ if(!strcmp(option, "name")){ - if(snd_seq_set_client_name(sequencer, value) < 0){ - fprintf(stderr, "Failed to set MIDI client name to %s\n", value); - return 1; - } + free(sequencer_name); + sequencer_name = strdup(value); return 0; } @@ -361,6 +352,21 @@ static int midi_start(){ return 0; } + //connect to the sequencer + if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){ + fprintf(stderr, "Failed to open ALSA sequencer\n"); + return 0; + } + + snd_seq_nonblock(sequencer, 1); + fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer)); + + //update the sequencer client name + if(snd_seq_set_client_name(sequencer, sequencer_name) < 0){ + fprintf(stderr, "Failed to set MIDI client name to %s\n", sequencer_name); + return 1; + } + //create all ports for(p = 0; p < n; p++){ data = (midi_instance_data*) inst[p]->impl; @@ -437,12 +443,17 @@ static int midi_shutdown(){ free(inst); //close midi - snd_seq_close(sequencer); - sequencer = NULL; + if(sequencer){ + snd_seq_close(sequencer); + sequencer = NULL; + } //free configuration cache snd_config_update_free_global(); + free(sequencer_name); + sequencer_name = NULL; + fprintf(stderr, "MIDI backend shut down\n"); return 0; } -- cgit v1.2.3 From ba8cb9bfaf0c3699a728cef6918d7433a8690936 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 24 Aug 2019 22:13:49 +0200 Subject: Fix NULL pointer access --- backends/midi.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/midi.c b/backends/midi.c index 88b841c..571a61d 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -362,7 +362,7 @@ static int midi_start(){ fprintf(stderr, "MIDI client ID is %d\n", snd_seq_client_id(sequencer)); //update the sequencer client name - if(snd_seq_set_client_name(sequencer, sequencer_name) < 0){ + if(snd_seq_set_client_name(sequencer, sequencer_name ? sequencer_name : "MIDIMonster") < 0){ fprintf(stderr, "Failed to set MIDI client name to %s\n", sequencer_name); return 1; } -- cgit v1.2.3 From 191949152d803229275d7b98b184ff722a9549a2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 31 Aug 2019 17:23:22 +0200 Subject: Update documentation --- README.md | 11 +++++++++-- backends/maweb.md | 2 ++ backends/midi.md | 3 +++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c4233f..cf4d8d1 100644 --- a/README.md +++ b/README.md @@ -143,12 +143,19 @@ support for the protocols to translate. * A C compiler * GNUmake +To build for Windows, the package `mingw-w64` provides a cross-compiler that can +be used to build a subset of the backends as well as the core. + ### Build -Just running `make` in the source directory should do the trick. +For Linux and OSX, just running `make` in the source directory should do the trick. Some backends have been marked as optional as they require rather large additional software to be installed, -for example the `ola` backend. To build these, run `make full` in the backends directory. +for example the `ola` backend. To create a build including these, run `make full`. + +To build on windows, install the crosscompiler package listed above and run +`make windows`. This will build `midimonster.exe` as well as a set of backends as DLL +files. ## Development diff --git a/backends/maweb.md b/backends/maweb.md index 9fe4790..93cd776 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -109,4 +109,6 @@ Data input from the console is done by actively querying the state of all mapped at low latency. A lower input interval value will produce data with lower latency, at the cost of network & CPU usage. Higher values will make the input "step" more, but will not consume as many CPU cycles and network bandwidth. +When requesting button executor events on the fader pages (execs 101 to 222) of a dot2 console, map at least one fader control from the 0 - 22 range or input will not work due to strange limitations in the MA Web API. + Command line events are sent, but I'm not sure they're being handled yet. diff --git a/backends/midi.md b/backends/midi.md index 5e295ee..108860e 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -52,6 +52,9 @@ midi1.ch0.pitch > midi2.ch1.pitch ``` #### Known bugs / problems +To access MIDI data, the user running MIDIMonster needs read & write access to the ALSA sequencer. +This can usually be done by adding this user to the `audio` system group. + Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are generated, which amount to the same thing according to the spec). This may be implemented as a configuration option at a later time. -- cgit v1.2.3 From 49855046f683deb7fdadc2c3d8caf513099b4803 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Sep 2019 20:26:24 +0200 Subject: Use platform-independent specifiers for printf calls --- TODO | 5 ++++- backends/artnet.c | 8 ++++---- backends/maweb.c | 34 ++++++++++++++++++---------------- backends/osc.c | 24 ++++++++++++------------ backends/sacn.c | 10 +++++----- config.c | 6 +++--- midimonster.h | 1 + portability.h | 4 ++++ 8 files changed, 51 insertions(+), 41 deletions(-) diff --git a/TODO b/TODO index d04773b..cba1c15 100644 --- a/TODO +++ b/TODO @@ -1,10 +1,13 @@ +winmidi +rename +release + MIDI NRPN keepalive channels per backend? mm_backend_start might get some arguments so they don't have to fetch them all the time mm_channel_resolver might get additional info about the mapping direction Note source in channel value struct Optimize core channel search (store backend offset) -Printing backend / Verbose mode mm_managed_fd.impl is not freed currently diff --git a/backends/artnet.c b/backends/artnet.c index 8e47d4f..e01ac94 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -32,7 +32,7 @@ static int artnet_listener(char* host, char* port){ return -1; } - fprintf(stderr, "ArtNet backend interface %lu bound to %s port %s\n", artnet_fds, host, port); + fprintf(stderr, "ArtNet backend interface %" PRIsize_t " bound to %s port %s\n", artnet_fds, host, port); artnet_fd[artnet_fds].fd = fd; artnet_fd[artnet_fds].output_instances = 0; artnet_fd[artnet_fds].output_instance = NULL; @@ -235,7 +235,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) artnet_instance_data* data = (artnet_instance_data*) inst->impl; if(!data->dest_len){ - fprintf(stderr, "ArtNet instance %s not enabled for output (%lu channel events)\n", inst->name, num); + fprintf(stderr, "ArtNet instance %s not enabled for output (%" PRIsize_t " channel events)\n", inst->name, num); return 0; } @@ -300,7 +300,7 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ } if(!chan){ - fprintf(stderr, "Active channel %lu on %s not known to core\n", p, inst->name); + fprintf(stderr, "Active channel %" PRIsize_t " on %s not known to core\n", p, inst->name); return 1; } @@ -447,7 +447,7 @@ static int artnet_start(){ } } - fprintf(stderr, "ArtNet backend registering %lu descriptors to core\n", artnet_fds); + fprintf(stderr, "ArtNet backend registering %" PRIsize_t " descriptors to core\n", artnet_fds); for(u = 0; u < artnet_fds; u++){ if(mm_manage_fd(artnet_fd[u].fd, BACKEND_NAME, 1, (void*) u)){ goto bail; diff --git a/backends/maweb.c b/backends/maweb.c index 80ab579..8d39f00 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -5,6 +5,8 @@ #include #endif +#define DEBUG + #include "libmmbackend.h" #include "maweb.h" @@ -338,7 +340,7 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty //ignore unused buttons return 0; } - fprintf(stderr, "maweb missing exec block data on exec %ld.%d\n", page, ident.fields.index); + fprintf(stderr, "maweb missing exec block data on exec %" PRIu64 ".%d\n", page, ident.fields.index); return 1; } @@ -367,7 +369,7 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty mm_channel_event(chan, evt); } - DBGPF("maweb page %ld exec %d value %f running %lu\n", page, ident.fields.index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); + DBGPF("maweb page %" PRIu64 " exec %d value %f running %" PRIu64 "\n", page, ident.fields.index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); ident.fields.index++; block++; } @@ -420,7 +422,7 @@ static int maweb_process_playbacks(instance* inst, int64_t page, char* payload, group++; } updates_inflight--; - DBGPF("maweb playback message processing done, %lu updates inflight\n", updates_inflight); + DBGPF("maweb playback message processing done, %" PRIu64 " updates inflight\n", updates_inflight); return 0; } @@ -433,7 +435,7 @@ static int maweb_request_playbacks(instance* inst){ size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels; if(updates_inflight){ - fprintf(stderr, "maweb skipping update request, %lu updates still inflight\n", updates_inflight); + fprintf(stderr, "maweb skipping update request, %" PRIu64 " updates still inflight\n", updates_inflight); return 0; } @@ -476,7 +478,7 @@ static int maweb_request_playbacks(instance* inst){ else{ //for the ma, the view equals the exec type requested (we can query all button execs from button view, all fader execs from fader view) view = (data->input_channel[channel].fields.index >= 100) ? 3 : 2; - snprintf(item_types, sizeof(item_types), "[%lu]", view); + snprintf(item_types, sizeof(item_types), "[%" PRIsize_t "]", view); //this channel must be included, so it must be in range for the first startindex snprintf(item_indices, sizeof(item_indices), "[%d]", (data->input_channel[channel].fields.index / 5) * 5); @@ -487,7 +489,7 @@ static int maweb_request_playbacks(instance* inst){ channels = data->input_channel[channel + channel_offset - 1].fields.index - (data->input_channel[channel].fields.index / 5) * 5; - snprintf(item_counts, sizeof(item_indices), "[%lu]", ((channels / 5) * 5 + 5)); + snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5)); } //advance base channel @@ -499,12 +501,12 @@ static int maweb_request_playbacks(instance* inst){ "\"requestType\":\"playbacks\"," "\"startIndex\":%s," "\"itemsCount\":%s," - "\"pageIndex\":%lu," + "\"pageIndex\":%" PRIsize_t "," "\"itemsType\":%s," - "\"view\":%lu," + "\"view\":%" PRIsize_t "," "\"execButtonViewMode\":2," //extended "\"buttonsViewMode\":0," //get vfader for button execs - "\"session\":%lu" + "\"session\":%" PRIu64 "}", item_indices, item_counts, @@ -546,16 +548,16 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le } } - DBGPF("maweb message (%lu): %s\n", payload_length, payload); + DBGPF("maweb message (%" PRIsize_t "): %s\n", payload_length, payload); if(json_obj(payload, "session") == JSON_NUMBER){ data->session = json_obj_int(payload, "session", data->session); - fprintf(stderr, "maweb session id is now %ld\n", data->session); + fprintf(stderr, "maweb session id is now %" PRIu64 "\n", data->session); } if(json_obj_bool(payload, "forceLogin", 0)){ fprintf(stderr, "maweb sending user credentials\n"); snprintf(xmit_buffer, sizeof(xmit_buffer), - "{\"requestType\":\"login\",\"username\":\"%s\",\"password\":\"%s\",\"session\":%ld}", + "{\"requestType\":\"login\",\"username\":\"%s\",\"password\":\"%s\",\"session\":%" PRIu64 "}", (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass, data->session); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } @@ -787,7 +789,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"pageIndex\":%d," "\"faderValue\":%f," "\"type\":1," - "\"session\":%ld" + "\"session\":%" PRIu64 "}", ident.fields.index, ident.fields.page, v[n].normalised, data->session); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; @@ -803,7 +805,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"pressed\":%s," "\"released\":%s," "\"type\":0," - "\"session\":%ld" + "\"session\":%" PRIu64 "}", ident.fields.index, ident.fields.page, (data->peer_type == peer_dot2 && ident.fields.type == exec_upper) ? 0 : (ident.fields.type - exec_button), (v[n].normalised > 0.9) ? "true" : "false", @@ -844,7 +846,7 @@ static int maweb_keepalive(){ for(u = 0; u < n; u++){ data = (maweb_instance_data*) inst[u]->impl; if(data->login){ - snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"session\":%ld}", data->session); + snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"session\":%" PRIu64 "}", data->session); maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } } @@ -926,7 +928,7 @@ static int maweb_start(){ return 0; } - fprintf(stderr, "maweb backend registering %lu descriptors to core\n", n); + fprintf(stderr, "maweb backend registering %" PRIsize_t " descriptors to core\n", n); //initialize timeouts last_keepalive = last_update = mm_timestamp(); diff --git a/backends/osc.c b/backends/osc.c index beb5527..bffbba8 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -239,20 +239,20 @@ static int osc_path_validate(char* path, uint8_t allow_patterns){ for(u = 0; u < strlen(path); u++){ for(c = 0; c < sizeof(illegal_chars); c++){ if(path[u] == illegal_chars[c]){ - fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, illegal_chars[c], u); + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, illegal_chars[c], u); return 1; } } if(!isgraph(path[u])){ - fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, pattern_chars[c], u); + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); return 1; } if(!allow_patterns){ for(c = 0; c < sizeof(pattern_chars); c++){ if(path[u] == pattern_chars[c]){ - fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, pattern_chars[c], u); + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); return 1; } } @@ -261,14 +261,14 @@ static int osc_path_validate(char* path, uint8_t allow_patterns){ switch(path[u]){ case '{': if(square_open || curly_open){ - fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, pattern_chars[c], u); + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); return 1; } curly_open = 1; break; case '[': if(square_open || curly_open){ - fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %lu\n", path, pattern_chars[c], u); + fprintf(stderr, "%s is not a valid OSC path: Illegal '%c' at %" PRIsize_t "\n", path, pattern_chars[c], u); return 1; } square_open = 1; @@ -477,14 +477,14 @@ static int osc_register_pattern(osc_instance_data* data, char* pattern_path, cha //parse min/max values token = strtok(NULL, " "); if(!token){ - fprintf(stderr, "Missing minimum specification for parameter %lu of OSC pattern %s\n", u, pattern_path); + fprintf(stderr, "Missing minimum specification for parameter %" PRIsize_t " of OSC pattern %s\n", u, pattern_path); return 1; } data->pattern[pattern].min[u] = osc_parse_value_spec(format[u], token); token = strtok(NULL, " "); if(!token){ - fprintf(stderr, "Missing maximum specification for parameter %lu of OSC pattern %s\n", u, pattern_path); + fprintf(stderr, "Missing maximum specification for parameter %" PRIsize_t " of OSC pattern %s\n", u, pattern_path); return 1; } data->pattern[pattern].max[u] = osc_parse_value_spec(format[u], token); @@ -686,7 +686,7 @@ static int osc_output_channel(instance* inst, size_t channel){ //write data if(offset + osc_data_length(data->channel[channel].type[p]) >= sizeof(xmit_buf)){ - fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %lu\n", inst->name, data->channel[channel].path, p); + fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s at parameter %" PRIsize_t "\n", inst->name, data->channel[channel].path, p); return 1; } @@ -717,7 +717,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ osc_instance_data* data = (osc_instance_data*) inst->impl; if(!data->dest_len){ - fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%lu channels)\n", inst->name, num); + fprintf(stderr, "OSC instance %s does not have a destination, output is disabled (%" PRIsize_t " channels)\n", inst->name, num); return 0; } @@ -775,7 +775,7 @@ static int osc_process_packet(instance* inst, char* local_path, char* format, ui channel* chan = NULL; if(payload_len % 4){ - fprintf(stderr, "Invalid OSC packet, data length %lu\n", payload_len); + fprintf(stderr, "Invalid OSC packet, data length %" PRIsize_t "\n", payload_len); return 0; } @@ -784,7 +784,7 @@ static int osc_process_packet(instance* inst, char* local_path, char* format, ui ident.fields.channel = c; //unconfigured input should work without errors (using default limits) if(data->channel[c].params && strlen(format) != data->channel[c].params){ - fprintf(stderr, "OSC message %s.%s had format %s, internal representation has %lu parameters\n", inst->name, local_path, format, data->channel[c].params); + fprintf(stderr, "OSC message %s.%s had format %s, internal representation has %" PRIsize_t " parameters\n", inst->name, local_path, format, data->channel[c].params); continue; } @@ -925,7 +925,7 @@ static int osc_start(){ } } - fprintf(stderr, "OSC backend registered %lu descriptors to core\n", fds); + fprintf(stderr, "OSC backend registered %" PRIsize_t " descriptors to core\n", fds); free(inst); return 0; diff --git a/backends/sacn.c b/backends/sacn.c index 470e3bd..6e1b20b 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -78,7 +78,7 @@ static int sacn_listener(char* host, char* port, uint8_t fd_flags){ return -1; } - fprintf(stderr, "sACN backend interface %lu bound to %s port %s\n", global_cfg.fds, host, port); + fprintf(stderr, "sACN backend interface %" PRIsize_t " bound to %s port %s\n", global_cfg.fds, host, port); global_cfg.fd[global_cfg.fds].fd = fd; global_cfg.fd[global_cfg.fds].flags = fd_flags; global_cfg.fd[global_cfg.fds].universes = 0; @@ -300,7 +300,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ } if(!data->xmit_prio){ - fprintf(stderr, "sACN instance %s not enabled for output (%lu channel events)\n", inst->name, num); + fprintf(stderr, "sACN instance %s not enabled for output (%" PRIsize_t " channel events)\n", inst->name, num); return 0; } @@ -385,7 +385,7 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame } if(!chan){ - fprintf(stderr, "Active channel %lu on %s not known to core", u, inst->name); + fprintf(stderr, "Active channel %" PRIsize_t " on %s not known to core", u, inst->name); return 1; } @@ -453,7 +453,7 @@ 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){ - fprintf(stderr, "Failed to output sACN universe discovery frame for interface %lu: %s\n", fd, strerror(errno)); + fprintf(stderr, "Failed to output sACN universe discovery frame for interface %" PRIsize_t ": %s\n", fd, strerror(errno)); } } } @@ -615,7 +615,7 @@ static int sacn_start(){ } } - fprintf(stderr, "sACN backend registering %lu descriptors to core\n", global_cfg.fds); + fprintf(stderr, "sACN backend registering %" PRIsize_t " descriptors to core\n", global_cfg.fds); for(u = 0; u < global_cfg.fds; u++){ //allocate memory for storing last frame transmission timestamp global_cfg.fd[u].last_frame = calloc(global_cfg.fd[u].universes, sizeof(uint64_t)); diff --git a/config.c b/config.c index 93fb56d..f4d928e 100644 --- a/config.c +++ b/config.c @@ -162,7 +162,7 @@ static int config_glob_scan(instance* inst, channel_spec* spec){ } if(!spec->internal){ //TODO try to parse globs externally - fprintf(stderr, "Failed to parse glob %lu in %s internally\n", u + 1, spec->spec); + fprintf(stderr, "Failed to parse glob %" PRIsize_t " in %s internally\n", u + 1, spec->spec); return 1; } @@ -199,7 +199,7 @@ static channel* config_glob_resolve(instance* inst, channel_spec* spec, uint64_t //write out value bytes = snprintf(resolved_spec + spec->glob[glob - 1].offset[0], glob_length, - "%lu", + "%" PRIu64, current_value); if(bytes > glob_length){ fprintf(stderr, "Internal error resolving glob %s\n", spec->spec); @@ -279,7 +279,7 @@ static int config_map(char* to_raw, char* from_raw){ if((spec_to.channels != spec_from.channels && spec_from.channels != 1 && spec_to.channels != 1) || spec_to.channels == 0 || spec_from.channels == 0){ - fprintf(stderr, "Multi-channel specification size mismatch: %s.%s (%lu channels) - %s.%s (%lu channels)\n", + fprintf(stderr, "Multi-channel specification size mismatch: %s.%s (%" PRIsize_t " channels) - %s.%s (%" PRIsize_t " channels)\n", instance_from->name, spec_from.spec, spec_from.channels, diff --git a/midimonster.h b/midimonster.h index 270a61f..491cc11 100644 --- a/midimonster.h +++ b/midimonster.h @@ -3,6 +3,7 @@ #include #include #include +#include #ifndef MM_API #ifdef _WIN32 diff --git a/portability.h b/portability.h index 903ecd8..f0bfd07 100644 --- a/portability.h +++ b/portability.h @@ -35,4 +35,8 @@ #define htole64(x) (x) #define be64toh(x) _byteswap_uint64(x) #define le64toh(x) (x) + + #define PRIsize_t "Iu" +#else + #define PRIsize_t "zu" #endif -- cgit v1.2.3 From bab255a969acc93fb7628b0827fa8f93208b6fa0 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Sep 2019 20:30:27 +0200 Subject: Don't build maweb as debug --- backends/maweb.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 8d39f00..2b7a37c 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -5,8 +5,6 @@ #include #endif -#define DEBUG - #include "libmmbackend.h" #include "maweb.h" -- cgit v1.2.3 From 1a23b9339c0164ec18fe6dc5eb900185e4f9d886 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Sep 2019 21:31:14 +0200 Subject: Update the list of supported protocols to a table --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cf4d8d1..25cdce0 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,17 @@ tool between multi-channel absolute-value-based control and/or bus protocols. Currently, the MIDIMonster supports the following protocols: -* MIDI (Linux, via ALSA) -* ArtNet -* Streaming ACN (sACN / E1.31) -* OpenSoundControl (OSC) -* evdev input devices (Linux) -* Open Lighting Architecture (OLA) -* MA Lighting Web Remote - -with additional flexibility provided by a Lua scripting environment. +| Protocol | Operating Systems | Notes | Backends | +|-------------------------------|-----------------------|-------------------------------|-------------------------------| +| MIDI | Linux | Via ALSA | [`midi`](backends/midi.md) | +| 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) | +| evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | +| Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | +| MA Lighting Web Remote | Linux, Windows, OSX | GrandMA and dot2 (incl. OnPC) | [`maweb`](backends/maweb.md) | + +with additional flexibility provided by a [Lua scripting environment](backends/lua.md). The MIDIMonster allows the user to translate any channel on one protocol into channel(s) on any other (or the same) supported protocol, for example to: -- cgit v1.2.3 From f19e3fe0d131c68d1e77fcd8706b260c28f508b8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 7 Sep 2019 14:20:00 +0200 Subject: maweb I/O buffering --- README.md | 6 +- backends/maweb.c | 245 ++++++++++++++++++++++++++++++++----------------------- backends/maweb.h | 27 +++--- 3 files changed, 158 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 25cdce0..33c4f5a 100644 --- a/README.md +++ b/README.md @@ -155,9 +155,9 @@ For Linux and OSX, just running `make` in the source directory should do the tri Some backends have been marked as optional as they require rather large additional software to be installed, for example the `ola` backend. To create a build including these, run `make full`. -To build on windows, install the crosscompiler package listed above and run -`make windows`. This will build `midimonster.exe` as well as a set of backends as DLL -files. +To build for Windows, you still need to compile on a Linux machine. +Install the crosscompiler package listed above and run `make windows`. +This will build `midimonster.exe` as well as a set of backends as DLL files. ## Development diff --git a/backends/maweb.c b/backends/maweb.c index 2b7a37c..ca8a47d 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -14,9 +14,6 @@ #define WS_FLAG_FIN 0x80 #define WS_FLAG_MASK 0x80 -//TODO test using different pages simultaneously -//TODO test dot2 button virtual faders in fader view - static uint64_t last_keepalive = 0; static uint64_t update_interval = 50; static uint64_t last_update = 0; @@ -104,11 +101,6 @@ int init(){ .interval = maweb_interval }; - if(sizeof(maweb_channel_ident) != sizeof(uint64_t)){ - fprintf(stderr, "maweb channel identification union out of bounds\n"); - return 1; - } - //register backend if(mm_backend_register(maweb)){ fprintf(stderr, "Failed to register maweb backend\n"); @@ -117,14 +109,31 @@ int init(){ return 0; } -static int channel_comparator(const void* raw_a, const void* raw_b){ - maweb_channel_ident* a = (maweb_channel_ident*) raw_a; - maweb_channel_ident* b = (maweb_channel_ident*) raw_b; - - if(a->fields.page != b->fields.page){ - return a->fields.page - b->fields.page; +static ssize_t maweb_channel_index(maweb_instance_data* data, maweb_channel_type type, uint16_t page, uint16_t index){ + size_t n; + for(n = 0; n < data->channels; n++){ + if(data->channel[n].type == type + && data->channel[n].page == page + && data->channel[n].index == index){ + return n; + } } - return a->fields.index - b->fields.index; + return -1; +} + +static int channel_comparator(const void* raw_a, const void* raw_b){ + maweb_channel_data* a = (maweb_channel_data*) raw_a; + maweb_channel_data* b = (maweb_channel_data*) raw_b; + + //this needs to take into account command line channels + //they need to be sorted last so that the channel poll logic works properly + if(a->type != b->type){ + return a->type - b->type; + } + if(a->page != b->page){ + return a->page - b->page; + } + return a->index - b->index; } static uint32_t maweb_interval(){ @@ -214,14 +223,14 @@ static instance* maweb_instance(){ static channel* maweb_channel(instance* inst, char* spec){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; - maweb_channel_ident ident = { - .label = 0 + maweb_channel_data chan = { + 0 }; char* next_token = NULL; size_t n; if(!strncmp(spec, "page", 4)){ - ident.fields.page = strtoul(spec + 4, &next_token, 10); + chan.page = strtoul(spec + 4, &next_token, 10); if(*next_token != '.'){ fprintf(stderr, "Failed to parse maweb channel spec %s: Missing separator\n", spec); return NULL; @@ -229,65 +238,57 @@ static channel* maweb_channel(instance* inst, char* spec){ next_token++; if(!strncmp(next_token, "fader", 5)){ - ident.fields.type = exec_fader; + chan.type = exec_fader; next_token += 5; } else if(!strncmp(next_token, "upper", 5)){ - ident.fields.type = exec_upper; + chan.type = exec_upper; next_token += 5; } else if(!strncmp(next_token, "lower", 5)){ - ident.fields.type = exec_lower; + chan.type = exec_lower; next_token += 5; } else if(!strncmp(next_token, "flash", 5)){ - ident.fields.type = exec_button; + chan.type = exec_button; next_token += 5; } else if(!strncmp(next_token, "button", 6)){ - ident.fields.type = exec_button; + chan.type = exec_button; next_token += 6; } - ident.fields.index = strtoul(next_token, NULL, 10); + chan.index = strtoul(next_token, NULL, 10); } else{ for(n = 0; n < sizeof(cmdline_keys) / sizeof(char*); n++){ - if(!strcmp(spec, cmdline_keys[n])){ - ident.fields.type = cmdline_button; - ident.fields.index = n + 1; - ident.fields.page = 1; + //FIXME this is broken for layerMode + if(!strcmp(spec, cmdline_keys[n]) || (*spec == 'l' && !strcmp(spec + 1, cmdline_keys[n]))){ + chan.type = (*spec == 'l') ? cmdline_local : cmdline; + chan.index = n + 1; + chan.page = 1; break; } } } - if(ident.fields.type && ident.fields.index && ident.fields.page){ + if(chan.type && chan.index && chan.page){ //actually, those are zero-indexed... - ident.fields.index--; - ident.fields.page--; - - //check if the (exec/meta) channel is already known - for(n = 0; n < data->input_channels; n++){ - if(data->input_channel[n].fields.page == ident.fields.page - && data->input_channel[n].fields.index == ident.fields.index){ - break; - } - } + chan.index--; + chan.page--; - //FIXME only register channels that are mapped as outputs - //only register exec channels for updates - if(n == data->input_channels && ident.fields.type != cmdline_button){ - data->input_channel = realloc(data->input_channel, (data->input_channels + 1) * sizeof(maweb_channel_ident)); - if(!data->input_channel){ + if(maweb_channel_index(data, chan.type, chan.page, chan.index) == -1){ + data->channel = realloc(data->channel, (data->channels + 1) * sizeof(maweb_channel_data)); + if(!data->channel){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; } - data->input_channel[n].label = ident.label; - data->input_channels++; + data->channel[data->channels] = chan; + data->channels++; } - return mm_channel(inst, ident.label, 1); + return mm_channel(inst, maweb_channel_index(data, chan.type, chan.page, chan.index), 1); } + fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec); return NULL; } @@ -325,20 +326,18 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload } static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control; - channel* chan = NULL; channel_value evt; - maweb_channel_ident ident = { - .fields.page = page - 1, - .fields.index = json_obj_int(payload, "iExec", 191) - }; + int64_t exec_index = json_obj_int(payload, "iExec", 191); + ssize_t channel_index; if(!exec_blocks){ if(metatype == 3){ //ignore unused buttons return 0; } - fprintf(stderr, "maweb missing exec block data on exec %" PRIu64 ".%d\n", page, ident.fields.index); + fprintf(stderr, "maweb missing exec block data on exec %" PRIu64 ".%" PRIu64 "\n", page, exec_index); return 1; } @@ -348,27 +347,41 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty } //TODO detect unused faders - //TODO state tracking for fader values / exec run state //iterate over executor blocks for(offset = json_array_offset(payload + exec_blocks, block); offset; offset = json_array_offset(payload + exec_blocks, block)){ control = exec_blocks + offset + json_obj_offset(payload + exec_blocks + offset, "fader"); - ident.fields.type = exec_fader; - chan = mm_channel(inst, ident.label, 0); - if(chan){ - evt.normalised = json_obj_double(payload + control, "v", 0.0); - mm_channel_event(chan, evt); + + channel_index = maweb_channel_index(data, exec_fader, page - 1, exec_index); + if(channel_index >= 0){ + if(!data->channel[channel_index].input_blocked){ + evt.normalised = json_obj_double(payload + control, "v", 0.0); + if(evt.normalised != data->channel[channel_index].in){ + mm_channel_event(mm_channel(inst, channel_index, 0), evt); + data->channel[channel_index].in = evt.normalised; + } + } + else{ + data->channel[channel_index].input_blocked--; + } } - ident.fields.type = exec_button; - chan = mm_channel(inst, ident.label, 0); - if(chan){ - evt.normalised = json_obj_int(payload, "isRun", 0); - mm_channel_event(chan, evt); + channel_index = maweb_channel_index(data, exec_button, page - 1, exec_index); + if(channel_index >= 0){ + if(!data->channel[channel_index].input_blocked){ + evt.normalised = json_obj_int(payload, "isRun", 0); + if(evt.normalised != data->channel[channel_index].in){ + mm_channel_event(mm_channel(inst, channel_index, 0), evt); + data->channel[channel_index].in = evt.normalised; + } + } + else{ + data->channel[channel_index].input_blocked--; + } } - DBGPF("maweb page %" PRIu64 " exec %d value %f running %" PRIu64 "\n", page, ident.fields.index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); - ident.fields.index++; + DBGPF("maweb page %" PRIu64 " exec %" PRIu64 " value %f running %" PRIu64 "\n", page, exec_index, json_obj_double(payload + control, "v", 0.0), json_obj_int(payload, "isRun", 0)); + exec_index++; block++; } @@ -437,33 +450,37 @@ static int maweb_request_playbacks(instance* inst){ return 0; } + //TODO this needs to ignore non-metamoded execs //don't quote me on this whole segment - for(channel = 0; channel < data->input_channels; channel++){ + + //only request faders and buttons + for(channel = 0; channel < data->channels && data->channel[channel].type <= exec_button; channel++){ offsets[0] = offsets[1] = offsets[2] = 1; - page_index = data->input_channel[channel].fields.page; + page_index = data->channel[channel].page; if(data->peer_type == peer_dot2){ //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view - view = (data->input_channel[channel].fields.index >= 300) ? 3 : 2; + view = (data->channel[channel].index >= 300) ? 3 : 2; - for(channel_offset = 1; channel + channel_offset <= data->input_channels; channel_offset++){ + for(channel_offset = 1; channel + channel_offset <= data->channels && data->channel[channel + channel_offset - 1].type < exec_button; channel_offset++){ channels = channel + channel_offset - 1; //find end for this exec block - for(; channel + channel_offset < data->input_channels; channel_offset++){ - if(data->input_channel[channel + channel_offset].fields.page != page_index - || (data->input_channel[channels].fields.index / 100) != (data->input_channel[channel + channel_offset].fields.index / 100)){ + for(; channel + channel_offset < data->channels; channel_offset++){ + if(data->channel[channel + channel_offset].page != page_index + || data->channel[channels].type != data->channel[channel + channel_offset].type + || (data->channel[channels].index / 100) != (data->channel[channel + channel_offset].index / 100)){ break; } } //add request block for the exec block - offsets[0] += snprintf(item_indices + offsets[0], sizeof(item_indices) - offsets[0], "%d,", data->input_channel[channels].fields.index); - offsets[1] += snprintf(item_counts + offsets[1], sizeof(item_counts) - offsets[1], "%d,", data->input_channel[channel + channel_offset - 1].fields.index - data->input_channel[channels].fields.index + 1); - offsets[2] += snprintf(item_types + offsets[2], sizeof(item_types) - offsets[2], "%d,", (data->input_channel[channels].fields.index < 100) ? 2 : 3); - - //send on page boundary, metamode boundary, last channel - if(channel + channel_offset >= data->input_channels - || data->input_channel[channel + channel_offset].fields.page != page_index - || (data->input_channel[channel].fields.index < 300) != (data->input_channel[channel + channel_offset].fields.index < 300)){ + offsets[0] += snprintf(item_indices + offsets[0], sizeof(item_indices) - offsets[0], "%d,", data->channel[channels].index); + offsets[1] += snprintf(item_counts + offsets[1], sizeof(item_counts) - offsets[1], "%d,", data->channel[channel + channel_offset - 1].index - data->channel[channels].index + 1); + offsets[2] += snprintf(item_types + offsets[2], sizeof(item_types) - offsets[2], "%d,", (data->channel[channels].index < 100) ? 2 : 3); + + //send on last channel, page boundary, metamode boundary + if(channel + channel_offset >= data->channels + || data->channel[channel + channel_offset].page != page_index + || (data->channel[channel].index < 300) != (data->channel[channel + channel_offset].index < 300)){ break; } } @@ -475,17 +492,18 @@ static int maweb_request_playbacks(instance* inst){ } else{ //for the ma, the view equals the exec type requested (we can query all button execs from button view, all fader execs from fader view) - view = (data->input_channel[channel].fields.index >= 100) ? 3 : 2; + view = (data->channel[channel].index >= 100) ? 3 : 2; snprintf(item_types, sizeof(item_types), "[%" PRIsize_t "]", view); //this channel must be included, so it must be in range for the first startindex - snprintf(item_indices, sizeof(item_indices), "[%d]", (data->input_channel[channel].fields.index / 5) * 5); + snprintf(item_indices, sizeof(item_indices), "[%d]", (data->channel[channel].index / 5) * 5); - for(channel_offset = 1; channel + channel_offset < data->input_channels - && data->input_channel[channel].fields.page == data->input_channel[channel + channel_offset].fields.page - && data->input_channel[channel].fields.index / 100 == data->input_channel[channel + channel_offset].fields.index / 100; channel_offset++){ + for(channel_offset = 1; channel + channel_offset < data->channels + && data->channel[channel].type == data->channel[channel + channel_offset].type + && data->channel[channel].page == data->channel[channel + channel_offset].page + && data->channel[channel].index / 100 == data->channel[channel + channel_offset].index / 100; channel_offset++){ } - channels = data->input_channel[channel + channel_offset - 1].fields.index - (data->input_channel[channel].fields.index / 5) * 5; + channels = data->channel[channel + channel_offset - 1].index - (data->channel[channel].index / 5) * 5; snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5)); } @@ -549,7 +567,12 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le DBGPF("maweb message (%" PRIsize_t "): %s\n", payload_length, payload); if(json_obj(payload, "session") == JSON_NUMBER){ data->session = json_obj_int(payload, "session", data->session); - fprintf(stderr, "maweb session id is now %" PRIu64 "\n", data->session); + if(data->session < 0){ + fprintf(stderr, "maweb login failed\n"); + data->login = 0; + return 0; + } + fprintf(stderr, "maweb session id is now %" PRId64 "\n", data->session); } if(json_obj_bool(payload, "forceLogin", 0)){ @@ -768,8 +791,8 @@ static int maweb_handle_fd(instance* inst){ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; + maweb_channel_data* chan = NULL; char xmit_buffer[MAWEB_XMIT_CHUNK]; - maweb_channel_ident ident; size_t n; if(num && !data->login){ @@ -778,8 +801,23 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ } for(n = 0; n < num; n++){ - ident.label = c[n]->ident; - switch(ident.fields.type){ + //sanity check + if(c[n]->ident >= data->channels){ + return 1; + } + chan = data->channel + c[n]->ident; + + //channel state tracking + if(chan->out == v[n].normalised){ + continue; + } + chan->out = v[n].normalised; + + //i/o value space separation + chan->in = v[n].normalised; + chan->input_blocked = 1; + + switch(chan->type){ case exec_fader: snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"playbacks_userInput\"," @@ -788,8 +826,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"faderValue\":%f," "\"type\":1," "\"session\":%" PRIu64 - "}", ident.fields.index, ident.fields.page, v[n].normalised, data->session); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + "}", chan->index, chan->page, v[n].normalised, data->session); break; case exec_upper: case exec_lower: @@ -804,26 +841,26 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ "\"released\":%s," "\"type\":0," "\"session\":%" PRIu64 - "}", ident.fields.index, ident.fields.page, - (data->peer_type == peer_dot2 && ident.fields.type == exec_upper) ? 0 : (ident.fields.type - exec_button), + "}", chan->index, chan->page, + (data->peer_type == peer_dot2 && chan->type == exec_upper) ? 0 : (chan->type - exec_button), (v[n].normalised > 0.9) ? "true" : "false", (v[n].normalised > 0.9) ? "false" : "true", data->session); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; - case cmdline_button: + case cmdline: snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"keyname\":\"%s\"," //"\"autoSubmit\":false," "\"value\":%d" - "}", cmdline_keys[ident.fields.index], + "}", cmdline_keys[chan->index], (v[n].normalised > 0.9) ? 1 : 0); - maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); break; + //TODO cmdline_local default: fprintf(stderr, "maweb control not yet implemented\n"); - break; + return 1; } + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } return 0; } @@ -912,7 +949,7 @@ static int maweb_start(){ for(u = 0; u < n; u++){ //sort channels data = (maweb_instance_data*) inst[u]->impl; - qsort(data->input_channel, data->input_channels, sizeof(maweb_channel_ident), channel_comparator); + qsort(data->channel, data->channels, sizeof(maweb_channel_data), channel_comparator); if(maweb_connect(inst[u])){ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); @@ -964,9 +1001,9 @@ static int maweb_shutdown(){ data->offset = data->allocated = 0; data->state = ws_new; - free(data->input_channel); - data->input_channel = NULL; - data->input_channels = 0; + free(data->channel); + data->channel = NULL; + data->channels = 0; } free(inst); diff --git a/backends/maweb.h b/backends/maweb.h index a868426..3738367 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -25,7 +25,8 @@ typedef enum /*_maweb_channel_type*/ { exec_button = 2, //gma: 0 dot: 0 exec_lower = 3, //gma: 1 dot: 1 exec_upper = 4, //gma: 2 dot: 0 - cmdline_button + cmdline, + cmdline_local } maweb_channel_type; typedef enum /*_maweb_peer_type*/ { @@ -49,15 +50,16 @@ typedef enum /*_ws_frame_op*/ { ws_pong = 10 } maweb_operation; -typedef union { - struct { - uint8_t padding[3]; - uint8_t type; - uint16_t page; - uint16_t index; - } fields; - uint64_t label; -} maweb_channel_ident; +typedef struct /*_maweb_channel*/ { + maweb_channel_type type; + uint16_t page; + uint16_t index; + + uint8_t input_blocked; + + double in; + double out; +} maweb_channel_data; typedef struct /*_maweb_instance_data*/ { char* host; @@ -69,9 +71,8 @@ typedef struct /*_maweb_instance_data*/ { int64_t session; maweb_peer_type peer_type; - //need to keep an internal registry to optimize data polls - size_t input_channels; - maweb_channel_ident* input_channel; + size_t channels; + maweb_channel_data* channel; int fd; maweb_state state; -- cgit v1.2.3 From 2822e7b6d10084adac5d35f26c2b158ccb91cd50 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Sep 2019 09:35:11 +0200 Subject: Fix duplicate poll requests --- backends/maweb.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index ca8a47d..905a1eb 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -346,8 +346,6 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty exec_blocks += json_obj_offset(payload + exec_blocks, "items"); } - //TODO detect unused faders - //iterate over executor blocks for(offset = json_array_offset(payload + exec_blocks, block); offset; offset = json_array_offset(payload + exec_blocks, block)){ control = exec_blocks + offset + json_obj_offset(payload + exec_blocks + offset, "fader"); @@ -362,6 +360,7 @@ static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_ty } } else{ + //block input immediately after channel set to prevent feedback loops data->channel[channel_index].input_blocked--; } } @@ -508,9 +507,19 @@ static int maweb_request_playbacks(instance* inst){ snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5)); } + DBGPF("maweb poll range first %d: %d.%d last %d: %d.%d next %d: %d.%d\n", + data->channel[channel].type, data->channel[channel].page, data->channel[channel].index, + data->channel[channel + channel_offset - 1].type, data->channel[channel + channel_offset - 1].page, data->channel[channel + channel_offset - 1].index, + data->channel[channel + channel_offset].type, data->channel[channel + channel_offset].page, data->channel[channel + channel_offset].index); + //advance base channel channel += channel_offset - 1; + //fader flash buttons have the same type as button execs, but can't be requested + if(data->channel[channel].index < 100 && data->channel[channel].type == exec_button){ + continue; + } + //send current request snprintf(xmit_buffer, sizeof(xmit_buffer), "{" -- cgit v1.2.3 From 9997c446677e490a0d376701f838e804c3b28644 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 14 Sep 2019 21:19:56 +0200 Subject: Fix maweb polling --- backends/maweb.c | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 905a1eb..88f7b9a 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -124,15 +124,24 @@ static ssize_t maweb_channel_index(maweb_instance_data* data, maweb_channel_type static int channel_comparator(const void* raw_a, const void* raw_b){ maweb_channel_data* a = (maweb_channel_data*) raw_a; maweb_channel_data* b = (maweb_channel_data*) raw_b; - + //this needs to take into account command line channels //they need to be sorted last so that the channel poll logic works properly - if(a->type != b->type){ - return a->type - b->type; - } if(a->page != b->page){ return a->page - b->page; } + //execs and their components are sorted by index first, type second + if(a->type < cmdline && b->type < cmdline){ + if(a->index != b->index){ + return a->index - b->index; + } + return a->type - b->type; + + } + //if either one is not an exec, sort by type first, index second + if(a->type != b->type){ + return a->type - b->type; + } return a->index - b->index; } @@ -153,9 +162,6 @@ static int maweb_configure(char* option, char* value){ static int maweb_configure_instance(instance* inst, char* option, char* value){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; char* host = NULL, *port = NULL; - #ifndef MAWEB_NO_LIBSSL - uint8_t password_hash[MD5_DIGEST_LENGTH]; - #endif if(!strcmp(option, "host")){ mmbackend_parse_hostspec(value, &host, &port); @@ -180,6 +186,8 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){ else if(!strcmp(option, "password")){ #ifndef MAWEB_NO_LIBSSL size_t n; + uint8_t password_hash[MD5_DIGEST_LENGTH]; + MD5((uint8_t*) value, strlen(value), (uint8_t*) password_hash); data->pass = realloc(data->pass, (2 * MD5_DIGEST_LENGTH + 1) * sizeof(char)); for(n = 0; n < MD5_DIGEST_LENGTH; n++){ @@ -192,7 +200,7 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){ #endif } - fprintf(stderr, "Unknown configuration parameter %s for manet instance %s\n", option, inst->name); + fprintf(stderr, "Unknown configuration parameter %s for maweb instance %s\n", option, inst->name); return 1; } @@ -328,9 +336,9 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload static int maweb_process_playback(instance* inst, int64_t page, maweb_channel_type metatype, char* payload, size_t payload_length){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; size_t exec_blocks = json_obj_offset(payload, (metatype == 2) ? "executorBlocks" : "bottomButtons"), offset, block = 0, control; - channel_value evt; int64_t exec_index = json_obj_int(payload, "iExec", 191); ssize_t channel_index; + channel_value evt; if(!exec_blocks){ if(metatype == 3){ @@ -449,23 +457,22 @@ static int maweb_request_playbacks(instance* inst){ return 0; } - //TODO this needs to ignore non-metamoded execs - //don't quote me on this whole segment - //only request faders and buttons - for(channel = 0; channel < data->channels && data->channel[channel].type <= exec_button; channel++){ + for(channel = 0; channel < data->channels && data->channel[channel].type < cmdline; channel++){ offsets[0] = offsets[1] = offsets[2] = 1; page_index = data->channel[channel].page; + //poll logic differs between the consoles because reasons + //dont quote me on this section if(data->peer_type == peer_dot2){ //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view view = (data->channel[channel].index >= 300) ? 3 : 2; - for(channel_offset = 1; channel + channel_offset <= data->channels && data->channel[channel + channel_offset - 1].type < exec_button; channel_offset++){ + for(channel_offset = 1; channel + channel_offset <= data->channels + && data->channel[channel + channel_offset].type < cmdline; channel_offset++){ channels = channel + channel_offset - 1; //find end for this exec block for(; channel + channel_offset < data->channels; channel_offset++){ if(data->channel[channel + channel_offset].page != page_index - || data->channel[channels].type != data->channel[channel + channel_offset].type || (data->channel[channels].index / 100) != (data->channel[channel + channel_offset].index / 100)){ break; } @@ -496,14 +503,14 @@ static int maweb_request_playbacks(instance* inst){ //this channel must be included, so it must be in range for the first startindex snprintf(item_indices, sizeof(item_indices), "[%d]", (data->channel[channel].index / 5) * 5); + //find end of exec block for(channel_offset = 1; channel + channel_offset < data->channels - && data->channel[channel].type == data->channel[channel + channel_offset].type && data->channel[channel].page == data->channel[channel + channel_offset].page && data->channel[channel].index / 100 == data->channel[channel + channel_offset].index / 100; channel_offset++){ } + //gma execs are grouped in blocks of 5 channels = data->channel[channel + channel_offset - 1].index - (data->channel[channel].index / 5) * 5; - snprintf(item_counts, sizeof(item_indices), "[%" PRIsize_t "]", ((channels / 5) * 5 + 5)); } @@ -515,11 +522,6 @@ static int maweb_request_playbacks(instance* inst){ //advance base channel channel += channel_offset - 1; - //fader flash buttons have the same type as button execs, but can't be requested - if(data->channel[channel].index < 100 && data->channel[channel].type == exec_button){ - continue; - } - //send current request snprintf(xmit_buffer, sizeof(xmit_buffer), "{" @@ -544,6 +546,7 @@ static int maweb_request_playbacks(instance* inst){ updates_inflight++; } + DBGPF("maweb poll request handling done, %" PRIu64 " updates requested\n", updates_inflight); return rv; } -- cgit v1.2.3 From 1c8d7c570678c2f4f3bf61529489336f9d085f8d Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 15 Sep 2019 23:09:44 +0200 Subject: Fix minor MIDI issues --- backends/midi.c | 27 +++++++++++++++------------ plugin.c | 22 ++++++++++++++++------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index 571a61d..65ce48a 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -84,14 +84,14 @@ static instance* midi_instance(){ return inst; } -static int midi_configure_instance(instance* instance, char* option, char* value){ - midi_instance_data* data = (midi_instance_data*) instance->impl; +static int midi_configure_instance(instance* inst, char* option, char* value){ + midi_instance_data* data = (midi_instance_data*) inst->impl; //FIXME maybe allow connecting more than one device if(!strcmp(option, "read")){ //connect input device if(data->read){ - fprintf(stderr, "MIDI port already connected to an input device\n"); + fprintf(stderr, "MIDI instance %s was already connected to an input device\n", inst->name); return 1; } data->read = strdup(value); @@ -100,7 +100,7 @@ static int midi_configure_instance(instance* instance, char* option, char* value else if(!strcmp(option, "write")){ //connect output device if(data->write){ - fprintf(stderr, "MIDI port already connected to an output device\n"); + fprintf(stderr, "MIDI instance %s was already connected to an output device\n", inst->name); return 1; } data->write = strdup(value); @@ -111,7 +111,7 @@ static int midi_configure_instance(instance* instance, char* option, char* value return 1; } -static channel* midi_channel(instance* instance, char* spec){ +static channel* midi_channel(instance* inst, char* spec){ midi_channel_ident ident = { .label = 0 }; @@ -142,13 +142,13 @@ static channel* midi_channel(instance* instance, char* spec){ old_syntax = 1; } else{ - fprintf(stderr, "Unknown MIDI channel specification %s\n", spec); + fprintf(stderr, "Unknown MIDI channel control type in %s\n", spec); return NULL; } ident.fields.channel = strtoul(channel, &channel, 10); if(ident.fields.channel > 15){ - fprintf(stderr, "MIDI channel out of range in channel spec %s\n", spec); + fprintf(stderr, "MIDI channel out of range in midi channel spec %s\n", spec); return NULL; } @@ -176,18 +176,22 @@ static channel* midi_channel(instance* instance, char* spec){ ident.fields.type = pressure; channel += 8; } - else if(!strncmp(channel, "pitch", 8)){ + else if(!strncmp(channel, "pitch", 5)){ ident.fields.type = pitchbend; } else if(!strncmp(channel, "aftertouch", 10)){ ident.fields.type = aftertouch; } + else{ + fprintf(stderr, "Unknown MIDI channel control type in %s\n", spec); + return NULL; + } } ident.fields.control = strtoul(channel, NULL, 10); if(ident.label){ - return mm_channel(instance, ident.label, 1); + return mm_channel(inst, ident.label, 1); } return NULL; @@ -196,13 +200,12 @@ static channel* midi_channel(instance* instance, char* spec){ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t u; snd_seq_event_t ev; - midi_instance_data* data; + midi_instance_data* data = (midi_instance_data*) inst->impl; midi_channel_ident ident = { .label = 0 }; for(u = 0; u < num; u++){ - data = (midi_instance_data*) c[u]->instance->impl; ident.label = c[u]->ident; snd_seq_ev_clear(&ev); @@ -334,7 +337,7 @@ static int midi_handle(size_t num, managed_fd* fds){ } static int midi_start(){ - size_t n, p; + size_t n = 0, p; int nfds, rv = 1; struct pollfd* pfds = NULL; instance** inst = NULL; diff --git a/plugin.c b/plugin.c index a452559..dd99041 100644 --- a/plugin.c +++ b/plugin.c @@ -24,7 +24,6 @@ static int plugin_attach(char* path, char* file){ plugin_init init = NULL; void* handle = NULL; char* lib = NULL; - char* error = NULL; lib = calloc(strlen(path) + strlen(file) + 1, sizeof(char)); if(!lib){ @@ -36,16 +35,15 @@ static int plugin_attach(char* path, char* file){ handle = dlopen(lib, RTLD_NOW); if(!handle){ #ifdef _WIN32 + 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); - #else - error = dlerror(); - #endif fprintf(stderr, "Failed to load plugin %s: %s\n", lib, error); - free(lib); - #ifdef _WIN32 LocalFree(error); + #else + fprintf(stderr, "Failed to load plugin %s: %s\n", lib, dlerror()); #endif + free(lib); return 0; } @@ -156,10 +154,22 @@ load_done: int plugins_close(){ size_t u; + for(u = 0; u < plugins; u++){ +#ifdef _WIN32 + char* error = NULL; + //FreeLibrary returns the inverse of dlclose + if(!FreeLibrary(plugin_handle[u])){ + 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 unload plugin: %s\n", error); + LocalFree(error); + } +#else if(dlclose(plugin_handle[u])){ fprintf(stderr, "Failed to unload plugin: %s\n", dlerror()); } +#endif } free(plugin_handle); -- cgit v1.2.3 From 1061c4a683df6ccef98c4307860d1c1db323131a Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Sep 2019 22:08:14 +0200 Subject: Publish winmidi backend --- README.md | 3 +- backends/Makefile | 5 +- backends/winmidi.c | 567 ++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/winmidi.h | 43 ++++ backends/winmidi.md | 59 ++++++ 5 files changed, 675 insertions(+), 2 deletions(-) create mode 100644 backends/winmidi.c create mode 100644 backends/winmidi.h create mode 100644 backends/winmidi.md diff --git a/README.md b/README.md index 33c4f5a..3cdac99 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Currently, the MIDIMonster supports the following protocols: | Protocol | Operating Systems | Notes | Backends | |-------------------------------|-----------------------|-------------------------------|-------------------------------| -| MIDI | Linux | Via ALSA | [`midi`](backends/midi.md) | +| MIDI | Linux, Windows | Linux: via ALSA | [`midi`](backends/midi.md), [`winmidi`](backends/winmidi.md) | | 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) | @@ -117,6 +117,7 @@ configuration options, channel specification syntax and any known problems or ot special information. These documentation files are located in the `backends/` directory. * [`midi` backend documentation](backends/midi.md) +* [`winmidi` backend documentation](backends/winmidi.md) * [`artnet` backend documentation](backends/artnet.md) * [`sacn` backend documentation](backends/sacn.md) * [`evdev` backend documentation](backends/evdev.md) diff --git a/backends/Makefile b/backends/Makefile index 5c5b677..293b434 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,6 +1,6 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.so -WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o @@ -38,6 +38,9 @@ maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) maweb.dll: LDLIBS += -lws2_32 maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL +winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +winmidi.dll: LDLIBS += -lwinmm -lws2_32 + midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) diff --git a/backends/winmidi.c b/backends/winmidi.c new file mode 100644 index 0000000..dd8442b --- /dev/null +++ b/backends/winmidi.c @@ -0,0 +1,567 @@ +#include + +#include "libmmbackend.h" +#include + +#define DEBUG +#include "winmidi.h" + +#define BACKEND_NAME "winmidi" + +static struct { + uint8_t list_devices; + int socket_pair[2]; + + CRITICAL_SECTION push_events; + volatile size_t events_alloc; + volatile size_t events_active; + volatile winmidi_event* event; +} backend_config = { + .list_devices = 0, + .socket_pair = {-1, -1} +}; + +//TODO allow connect-device specification by index +//TODO detect option + +int init(){ + backend winmidi = { + .name = BACKEND_NAME, + .conf = winmidi_configure, + .create = winmidi_instance, + .conf_instance = winmidi_configure_instance, + .channel = winmidi_channel, + .handle = winmidi_set, + .process = winmidi_handle, + .start = winmidi_start, + .shutdown = winmidi_shutdown + }; + + if(sizeof(winmidi_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "winmidi channel identification union out of bounds\n"); + return 1; + } + + //register backend + if(mm_backend_register(winmidi)){ + fprintf(stderr, "Failed to register winmidi backend\n"); + return 1; + } + + //initialize critical section + InitializeCriticalSectionAndSpinCount(&backend_config.push_events, 4000); + return 0; +} + +static int winmidi_configure(char* option, char* value){ + if(!strcmp(option, "list")){ + backend_config.list_devices = 0; + if(!strcmp(value, "on")){ + backend_config.list_devices = 1; + } + return 0; + } + + fprintf(stderr, "Unknown winmidi backend option %s\n", option); + return 1; +} + +static int winmidi_configure_instance(instance* inst, char* option, char* value){ + winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; + if(!strcmp(option, "read")){ + if(data->read){ + fprintf(stderr, "winmidi instance %s already connected to an input device\n", inst->name); + return 1; + } + data->read = strdup(value); + return 0; + } + if(!strcmp(option, "write")){ + if(data->write){ + fprintf(stderr, "winmidi instance %s already connected to an otput device\n", inst->name); + return 1; + } + data->write = strdup(value); + return 0; + } + + fprintf(stderr, "Unknown winmidi instance option %s\n", option); + return 1; +} + +static instance* winmidi_instance(){ + instance* i = mm_instance(); + if(!i){ + return NULL; + } + + i->impl = calloc(1, sizeof(winmidi_instance_data)); + if(!i->impl){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + return i; +} + +static channel* winmidi_channel(instance* inst, char* spec){ + char* next_token = NULL; + winmidi_channel_ident ident = { + .label = 0 + }; + + if(!strncmp(spec, "ch", 2)){ + next_token = spec + 2; + if(!strncmp(spec, "channel", 7)){ + next_token = spec + 7; + } + } + else{ + fprintf(stderr, "Unknown winmidi channel specification %s\n", spec); + return NULL; + } + + ident.fields.channel = strtoul(next_token, &next_token, 10); + if(ident.fields.channel > 15){ + fprintf(stderr, "MIDI channel out of range in winmidi channel spec %s\n", spec); + return NULL; + } + + if(*next_token != '.'){ + fprintf(stderr, "winmidi channel specification %s does not conform to channel.\n", spec); + return NULL; + } + + next_token++; + + if(!strncmp(next_token, "cc", 2)){ + ident.fields.type = cc; + next_token += 2; + } + else if(!strncmp(next_token, "note", 4)){ + ident.fields.type = note; + next_token += 4; + } + else if(!strncmp(next_token, "pressure", 8)){ + ident.fields.type = pressure; + next_token += 8; + } + else if(!strncmp(next_token, "pitch", 5)){ + ident.fields.type = pitchbend; + } + else if(!strncmp(next_token, "aftertouch", 10)){ + ident.fields.type = aftertouch; + } + else{ + fprintf(stderr, "Unknown winmidi channel control type in %s\n", spec); + return NULL; + } + + ident.fields.control = strtoul(next_token, NULL, 10); + + if(ident.label){ + return mm_channel(inst, ident.label, 1); + } + return NULL; +} + +static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ + winmidi_instance_data* data = (winmidi_instance_data*) inst->impl; + winmidi_channel_ident ident = { + .label = 0 + }; + union { + struct { + uint8_t status; + uint8_t data1; + uint8_t data2; + uint8_t unused; + } components; + DWORD dword; + } output = { + .dword = 0 + }; + size_t u; + + if(!data->device_out){ + fprintf(stderr, "winmidi instance %s has no output device\n", inst->name); + return 0; + } + + for(u = 0; u < num; u++){ + ident.label = c[u]->ident; + + switch(ident.fields.type){ + case note: + output.components.status = 0x90 | ident.fields.channel; + output.components.data1 = ident.fields.control; + output.components.data2 = v[u].normalised * 127.0; + break; + case cc: + output.components.status = 0xB0 | ident.fields.channel; + output.components.data1 = ident.fields.control; + output.components.data2 = v[u].normalised * 127.0; + break; + case pressure: + output.components.status = 0xA0 | ident.fields.channel; + output.components.data1 = ident.fields.control; + output.components.data2 = v[u].normalised * 127.0; + break; + case aftertouch: + output.components.status = 0xD0 | ident.fields.channel; + output.components.data1 = v[u].normalised * 127.0; + output.components.data2 = 0; + break; + case pitchbend: + output.components.status = 0xE0 | ident.fields.channel; + output.components.data1 = ((int)(v[u].normalised * 32639.0)) & 0xFF; + output.components.data2 = (((int)(v[u].normalised * 32639.0)) & 0xFF00) >> 8; + break; + default: + fprintf(stderr, "Unknown winmidi channel type %d\n", ident.fields.type); + continue; + } + + midiOutShortMsg(data->device_out, output.dword); + } + + return 0; +} + +static int winmidi_handle(size_t num, managed_fd* fds){ + size_t u; + ssize_t bytes = 0; + char recv_buf[1024]; + channel* chan = NULL; + if(!num){ + return 0; + } + + //flush the feedback socket + for(u = 0; u < num; u++){ + bytes += recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); + } + + //push queued events + EnterCriticalSection(&backend_config.push_events); + for(u = 0; u < backend_config.events_active; u++){ + chan = mm_channel(backend_config.event[u].inst, backend_config.event[u].channel.label, 0); + if(chan){ + mm_channel_event(chan, backend_config.event[u].value); + } + } + DBGPF("winmidi flushed %" PRIsize_t " wakeups, handled %" PRIsize_t " events\n", bytes, backend_config.events_active); + backend_config.events_active = 0; + LeaveCriticalSection(&backend_config.push_events); + return 0; +} + +static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DWORD_PTR inst, DWORD param1, DWORD param2){ + winmidi_channel_ident ident = { + .label = 0 + }; + channel_value val; + union { + struct { + uint8_t status; + uint8_t data1; + uint8_t data2; + uint8_t unused; + } components; + DWORD dword; + } input = { + .dword = 0 + }; + + //callbacks may run on different threads, so we queue all events and alert the main thread via the feedback socket + DBGPF("winmidi input callback on thread %ld\n", GetCurrentThreadId()); + + switch(message){ + case MIM_MOREDATA: + //processing too slow, do not immediately alert the main loop + case MIM_DATA: + //param1 has the message + input.dword = param1; + ident.fields.channel = input.components.status & 0x0F; + switch(input.components.status & 0xF0){ + case 0x80: + ident.fields.type = note; + ident.fields.control = input.components.data1; + val.normalised = 0.0; + break; + case 0x90: + ident.fields.type = note; + ident.fields.control = input.components.data1; + val.normalised = (double) input.components.data2 / 127.0; + break; + case 0xA0: + ident.fields.type = pressure; + ident.fields.control = input.components.data1; + val.normalised = (double) input.components.data2 / 127.0; + break; + case 0xB0: + ident.fields.type = cc; + ident.fields.control = input.components.data1; + val.normalised = (double) input.components.data2 / 127.0; + break; + case 0xD0: + ident.fields.type = aftertouch; + ident.fields.control = 0; + val.normalised = (double) input.components.data1 / 127.0; + break; + case 0xE0: + ident.fields.type = pitchbend; + ident.fields.control = 0; + val.normalised = (double)((input.components.data2 << 8) | input.components.data1) / 32639.0; + break; + default: + fprintf(stderr, "winmidi unhandled status byte %02X\n", input.components.status); + return; + } + break; + case MIM_LONGDATA: + //sysex message, ignore + return; + case MIM_ERROR: + //error in input stream + fprintf(stderr, "winmidi warning: error in input stream\n"); + return; + case MIM_OPEN: + case MIM_CLOSE: + //device opened/closed + return; + + } + + DBGPF("winmidi incoming message type %d channel %d control %d value %f\n", + ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised); + + EnterCriticalSection(&backend_config.push_events); + if(backend_config.events_alloc <= backend_config.events_active){ + backend_config.event = realloc((void*) backend_config.event, (backend_config.events_alloc + 1) * sizeof(winmidi_event)); + if(!backend_config.event){ + fprintf(stderr, "Failed to allocate memory\n"); + backend_config.events_alloc = 0; + backend_config.events_active = 0; + LeaveCriticalSection(&backend_config.push_events); + return; + } + backend_config.events_alloc++; + } + backend_config.event[backend_config.events_active].inst = (instance*) inst; + backend_config.event[backend_config.events_active].channel.label = ident.label; + backend_config.event[backend_config.events_active].value = val; + backend_config.events_active++; + LeaveCriticalSection(&backend_config.push_events); + + if(message != MIM_MOREDATA){ + //alert the main loop + send(backend_config.socket_pair[1], "w", 1, 0); + } +} + +static void CALLBACK winmidi_output_callback(HMIDIOUT device, unsigned message, DWORD_PTR inst, DWORD param1, DWORD param2){ + DBGPF("winmidi output callback on thread %ld\n", GetCurrentThreadId()); +} + +static int winmidi_match_input(char* prefix){ + MIDIINCAPS input_caps; + unsigned inputs = midiInGetNumDevs(); + char* next_token = NULL; + size_t n; + + if(!prefix){ + fprintf(stderr, "winmidi detected %u input devices\n", inputs); + } + else{ + n = strtoul(prefix, &next_token, 10); + if(!(*next_token) && n < inputs){ + midiInGetDevCaps(n, &input_caps, sizeof(MIDIINCAPS)); + fprintf(stderr, "winmidi selected input device %s for ID %d\n", input_caps.szPname, n); + return n; + } + } + + //find prefix match for input device + for(n = 0; n < inputs; n++){ + midiInGetDevCaps(n, &input_caps, sizeof(MIDIINCAPS)); + if(!prefix){ + printf("\tID %d: %s\n", n, input_caps.szPname); + } + else if(!strncmp(input_caps.szPname, prefix, strlen(prefix))){ + fprintf(stderr, "winmidi selected input device %s for name %s\n", input_caps.szPname, prefix); + return n; + } + } + + return -1; +} + +static int winmidi_match_output(char* prefix){ + MIDIOUTCAPS output_caps; + unsigned outputs = midiOutGetNumDevs(); + char* next_token = NULL; + size_t n; + + if(!prefix){ + fprintf(stderr, "winmidi detected %u output devices\n", outputs); + } + else{ + n = strtoul(prefix, &next_token, 10); + if(!(*next_token) && n < outputs){ + midiOutGetDevCaps(n, &output_caps, sizeof(MIDIOUTCAPS)); + fprintf(stderr, "winmidi selected output device %s for ID %d\n", output_caps.szPname, n); + return n; + } + } + + //find prefix match for output device + for(n = 0; n < outputs; n++){ + midiOutGetDevCaps(n, &output_caps, sizeof(MIDIOUTCAPS)); + if(!prefix){ + printf("\tID %d: %s\n", n, output_caps.szPname); + } + else if(!strncmp(output_caps.szPname, prefix, strlen(prefix))){ + fprintf(stderr, "winmidi selected output device %s for name %s\n", output_caps.szPname, prefix); + return n; + } + } + + return -1; +} + +static int winmidi_start(){ + size_t n = 0, p; + int device, rv = -1; + instance** inst = NULL; + winmidi_instance_data* data = NULL; + struct sockaddr_storage sockadd; + //this really should be a size_t but getsockname specifies int* for some reason + int sockadd_len = sizeof(sockadd); + DBGPF("winmidi main thread ID is %ld\n", GetCurrentThreadId()); + + //fetch all instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + //no instances, we're done + if(!n){ + free(inst); + return 0; + } + + //output device list if requested + if(backend_config.list_devices){ + winmidi_match_input(NULL); + winmidi_match_output(NULL); + } + + //open the feedback sockets + backend_config.socket_pair[0] = mmbackend_socket(NULL, "0", SOCK_DGRAM, 1, 0); + if(backend_config.socket_pair[0] < 0){ + fprintf(stderr, "winmidi failed to open feedback socket\n"); + return 1; + } + if(getsockname(backend_config.socket_pair[0], (struct sockaddr*) &sockadd, &sockadd_len)){ + fprintf(stderr, "winmidi failed to query feedback socket information\n"); + return 1; + } + 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)){ + fprintf(stderr, "winmidi failed to connect to feedback socket\n"); + return 1; + } + + //set up instances and start input + for(p = 0; p < n; p++){ + data = (winmidi_instance_data*) inst[p]->impl; + inst[p]->ident = p; + + //connect input device if requested + if(data->read){ + device = winmidi_match_input(data->read); + if(device < 0){ + fprintf(stderr, "Failed to match input device %s for instance %s\n", data->read, inst[p]->name); + goto bail; + } + if(midiInOpen(&(data->device_in), device, (DWORD_PTR) winmidi_input_callback, (DWORD_PTR) inst[p], CALLBACK_FUNCTION | MIDI_IO_STATUS) != MMSYSERR_NOERROR){ + fprintf(stderr, "Failed to open input device for instance %s\n", inst[p]->name); + goto bail; + } + //start midi input callbacks + midiInStart(data->device_in); + } + + //connect output device if requested + if(data->write){ + device = winmidi_match_output(data->write); + if(device < 0){ + fprintf(stderr, "Failed to match output device %s for instance %s\n", data->read, inst[p]->name); + goto bail; + } + if(midiOutOpen(&(data->device_out), device, (DWORD_PTR) winmidi_output_callback, (DWORD_PTR) inst[p], CALLBACK_FUNCTION) != MMSYSERR_NOERROR){ + fprintf(stderr, "Failed to open output device for instance %s\n", inst[p]->name); + goto bail; + } + } + } + + //register the feedback socket to the core + fprintf(stderr, "winmidi backend registering 1 descriptor to core\n"); + if(mm_manage_fd(backend_config.socket_pair[0], BACKEND_NAME, 1, NULL)){ + goto bail; + } + + rv = 0; +bail: + free(inst); + return rv; +} + +static int winmidi_shutdown(){ + size_t n, u; + instance** inst = NULL; + winmidi_instance_data* data = NULL; + + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(u = 0; u < n; u++){ + data = (winmidi_instance_data*) inst[u]->impl; + free(data->read); + data->read = NULL; + free(data->write); + data->write = NULL; + + if(data->device_in){ + midiInStop(data->device_in); + midiInClose(data->device_in); + data->device_in = NULL; + } + + if(data->device_out){ + midiOutReset(data->device_out); + midiOutClose(data->device_out); + data->device_out = NULL; + } + } + + free(inst); + closesocket(backend_config.socket_pair[0]); + closesocket(backend_config.socket_pair[1]); + + EnterCriticalSection(&backend_config.push_events); + free((void*) backend_config.event); + backend_config.event = NULL; + backend_config.events_alloc = 0; + backend_config.events_active = 0; + LeaveCriticalSection(&backend_config.push_events); + DeleteCriticalSection(&backend_config.push_events); + + fprintf(stderr, "winmidi backend shut down\n"); + return 0; +} diff --git a/backends/winmidi.h b/backends/winmidi.h new file mode 100644 index 0000000..e4abda1 --- /dev/null +++ b/backends/winmidi.h @@ -0,0 +1,43 @@ +#include "midimonster.h" + +int init(); +static int winmidi_configure(char* option, char* value); +static int winmidi_configure_instance(instance* inst, char* option, char* value); +static instance* winmidi_instance(); +static channel* winmidi_channel(instance* inst, char* spec); +static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v); +static int winmidi_handle(size_t num, managed_fd* fds); +static int winmidi_start(); +static int winmidi_shutdown(); + +typedef struct /*_winmidi_instance_data*/ { + char* read; + char* write; + HMIDIIN device_in; + HMIDIOUT device_out; +} winmidi_instance_data; + +enum /*_winmidi_channel_type*/ { + none = 0, + note, + cc, + pressure, + aftertouch, + pitchbend +}; + +typedef union { + struct { + uint8_t pad[5]; + uint8_t type; + uint8_t channel; + uint8_t control; + } fields; + uint64_t label; +} winmidi_channel_ident; + +typedef struct /*_winmidi_event_queue_entry*/ { + instance* inst; + winmidi_channel_ident channel; + channel_value value; +} winmidi_event; diff --git a/backends/winmidi.md b/backends/winmidi.md new file mode 100644 index 0000000..b1fde1e --- /dev/null +++ b/backends/winmidi.md @@ -0,0 +1,59 @@ +### The `winmidi` backend + +This backend provides read-write access to the MIDI protocol via the Windows Multimedia API. + +It is only available when building for Windows. Care has been taken to keep the configuration +syntax similar to the `midi` backend, but due to differences in the internal programming interfaces, +some deviations may still be present. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `list` | `on` | `off` | List available input/output devices on startup | +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `read` | `2` | none | MIDI device to connect for input | +| `write` | `DeviceName` | none | MIDI device to connect for output | + +MIDI device names may either be prefixes of MIDI device names or a numeric index corresponding to the list output at startup using the backend `list` option. + +#### Channel specification + +The MIDI backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are + +* `cc` - Control Changes +* `note` - Note On/Off messages +* `pressure` - Note pressure/aftertouch messages +* `aftertouch` - Channel-wide aftertouch messages +* `pitch` - Channel pitchbend messages + +A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be +used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). + +The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. + +MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which +additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called +'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. + +Example mappings: +``` +midi1.ch0.note9 > midi2.channel1.cc4 +midi1.channel15.pressure1 > midi1.channel0.note0 +midi1.ch1.aftertouch > midi2.ch2.cc0 +midi1.ch0.pitch > midi2.ch1.pitch +``` + +#### Known bugs / problems + +Currently, no Note Off messages are sent (instead, Note On messages with a velocity of 0 are +generated, which amount to the same thing according to the spec). This may be implemented as +a configuration option at a later time. + +As this is a Windows-only backend, testing may not be as frequent or thorough as for the Linux / multiplatform +backends. -- cgit v1.2.3 From 678396ed4c1a146a110cb15c7b7ad8cf3d6ef224 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Sep 2019 22:13:30 +0200 Subject: Minor documentation fixes --- backends/winmidi.c | 1 - backends/winmidi.md | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backends/winmidi.c b/backends/winmidi.c index dd8442b..e121add 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -21,7 +21,6 @@ static struct { .socket_pair = {-1, -1} }; -//TODO allow connect-device specification by index //TODO detect option int init(){ diff --git a/backends/winmidi.md b/backends/winmidi.md index b1fde1e..f648240 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -20,11 +20,12 @@ some deviations may still be present. | `read` | `2` | none | MIDI device to connect for input | | `write` | `DeviceName` | none | MIDI device to connect for output | -MIDI device names may either be prefixes of MIDI device names or a numeric index corresponding to the list output at startup using the backend `list` option. +MIDI device names may either be prefixes of MIDI device names or a numeric indices corresponding to the listing shown +at startup when using the global `list` option. #### Channel specification -The MIDI backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are +The `winmidi` backend supports mapping different MIDI events as MIDIMonster channels. The currently supported event types are * `cc` - Control Changes * `note` - Note On/Off messages -- cgit v1.2.3 From 69f3115df4ff60fdef28408eb89d1b505633fcb4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Sep 2019 22:17:43 +0200 Subject: Add windows build to CI --- .travis-ci.sh | 5 +++++ .travis.yml | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/.travis-ci.sh b/.travis-ci.sh index ddfa93d..da36c17 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -71,6 +71,11 @@ elif [[ $TASK = 'sanitize' ]]; then travis_fold start "make_sanitize" make sanitize; travis_fold end "make_sanitize" +elif [[ $TASK = 'windows' ]]; then + # Run sanitized compile + travis_fold start "make_windows" + make windows; + travis_fold end "make_windows" else # Otherwise compile as normal travis_fold start "make" diff --git a/.travis.yml b/.travis.yml index 864c628..ced21ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,10 @@ addons: packages: &core_build_clang_latest - *core_build - clang-6.0 + packages: &core_build_windows + - *core_build + - mingw-w64 + matrix: fast_finish: true @@ -69,6 +73,16 @@ matrix: - *core_build_gpp_latest sources: - ubuntu-toolchain-r-test + - os: linux + dist: xenial + compiler: mingw-gcc + env: TASK='windows' + addons: + apt: + packages: + - *core_build_windows + sources: + - ubuntu-toolchain-r-test - os: linux dist: xenial compiler: clang -- cgit v1.2.3 From 3c44cd1ba564e9816f5cfb94fff6be5dca0ada3d Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Sep 2019 22:18:50 +0200 Subject: Fix typo --- backends/winmidi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/winmidi.md b/backends/winmidi.md index f648240..25a6378 100644 --- a/backends/winmidi.md +++ b/backends/winmidi.md @@ -20,8 +20,8 @@ some deviations may still be present. | `read` | `2` | none | MIDI device to connect for input | | `write` | `DeviceName` | none | MIDI device to connect for output | -MIDI device names may either be prefixes of MIDI device names or a numeric indices corresponding to the listing shown -at startup when using the global `list` option. +Input/output device names may either be prefixes of MIDI device names or numeric indices corresponding +to the listing shown at startup when using the global `list` option. #### Channel specification -- cgit v1.2.3 From 262210090bed5967157ce67dc1b52d43ba9cbdb9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Sep 2019 22:23:41 +0200 Subject: Don't build winmidi with DEBUG --- backends/winmidi.c | 1 - 1 file changed, 1 deletion(-) diff --git a/backends/winmidi.c b/backends/winmidi.c index e121add..2c52a22 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -3,7 +3,6 @@ #include "libmmbackend.h" #include -#define DEBUG #include "winmidi.h" #define BACKEND_NAME "winmidi" -- cgit v1.2.3 From cd59a560d8b182ce50ea1edae1c0f2a0a89e76c0 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 18 Sep 2019 00:57:35 +0200 Subject: Fix winmidi feedback connection --- .travis.yml | 2 +- backends/maweb.c | 2 +- backends/winmidi.c | 39 +++++++++++++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index ced21ef..ea4bd94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -75,7 +75,7 @@ matrix: - ubuntu-toolchain-r-test - os: linux dist: xenial - compiler: mingw-gcc + compiler: x86_64-w64-mingw32-gcc env: TASK='windows' addons: apt: diff --git a/backends/maweb.c b/backends/maweb.c index 88f7b9a..c88ca8c 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -591,7 +591,7 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le fprintf(stderr, "maweb sending user credentials\n"); snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"requestType\":\"login\",\"username\":\"%s\",\"password\":\"%s\",\"session\":%" PRIu64 "}", - (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass, data->session); + (data->peer_type == peer_dot2) ? "remote" : data->user, data->pass ? data->pass : MAWEB_DEFAULT_PASSWORD, data->session); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } if(json_obj(payload, "status") && json_obj(payload, "appType")){ diff --git a/backends/winmidi.c b/backends/winmidi.c index 2c52a22..4c72c32 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -181,6 +181,11 @@ 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){ fprintf(stderr, "winmidi instance %s has no output device\n", inst->name); return 0; @@ -433,9 +438,12 @@ static int winmidi_start(){ int device, rv = -1; instance** inst = NULL; winmidi_instance_data* data = NULL; - struct sockaddr_storage sockadd; + struct sockaddr_storage sockadd = { + 0 + }; //this really should be a size_t but getsockname specifies int* for some reason int sockadd_len = sizeof(sockadd); + char* error = NULL; DBGPF("winmidi main thread ID is %ld\n", GetCurrentThreadId()); //fetch all instances @@ -457,18 +465,41 @@ static int winmidi_start(){ } //open the feedback sockets - backend_config.socket_pair[0] = mmbackend_socket(NULL, "0", SOCK_DGRAM, 1, 0); + //for some reason this while construct fails to work on 'real' windows with ipv6 + backend_config.socket_pair[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); if(backend_config.socket_pair[0] < 0){ fprintf(stderr, "winmidi failed to open feedback socket\n"); return 1; } if(getsockname(backend_config.socket_pair[0], (struct sockaddr*) &sockadd, &sockadd_len)){ - fprintf(stderr, "winmidi failed to query feedback socket information\n"); + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + fprintf(stderr, "winmidi failed to query feedback socket information: %s\n", error); + LocalFree(error); return 1; } + //getsockname on 'real' windows may not set the adress - works on wine, though + switch(sockadd.ss_family){ + case AF_INET: + case AF_INET6: + ((struct sockaddr_in*) &sockadd)->sin_family = AF_INET; + ((struct sockaddr_in*) &sockadd)->sin_addr.s_addr = htobe32(INADDR_LOOPBACK); + break; + //for some absurd reason 'real' windows announces the socket as AF_INET6 but rejects any connection unless its AF_INET +// case AF_INET6: +// ((struct sockaddr_in6*) &sockadd)->sin6_addr = in6addr_any; +// break; + default: + fprintf(stderr, "winmidi invalid feedback socket family\n"); + return 1; + } + DBGPF("winmidi feedback socket family %d port %d\n", sockadd.ss_family, be16toh(((struct sockaddr_in*)&sockadd)->sin_port)); backend_config.socket_pair[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP); if(backend_config.socket_pair[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){ - fprintf(stderr, "winmidi failed to connect to feedback socket\n"); + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + fprintf(stderr, "winmidi failed to connect to feedback socket: %s\n", error); + LocalFree(error); return 1; } -- cgit v1.2.3 From 82f6b03929315d65693155d3274a79d4a6285896 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 18 Sep 2019 01:10:26 +0200 Subject: Fix spelling & optics --- .travis.yml | 6 ++++-- backends/winmidi.c | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index ea4bd94..12de8c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -75,8 +75,10 @@ matrix: - ubuntu-toolchain-r-test - os: linux dist: xenial - compiler: x86_64-w64-mingw32-gcc - env: TASK='windows' + compiler: mingw32-gcc + env: + - TASK='windows' + - CC='x86_64-w64-mingw32-gcc' addons: apt: packages: diff --git a/backends/winmidi.c b/backends/winmidi.c index 4c72c32..67187ac 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -478,7 +478,7 @@ static int winmidi_start(){ LocalFree(error); return 1; } - //getsockname on 'real' windows may not set the adress - works on wine, though + //getsockname on 'real' windows may not set the address - works on wine, though switch(sockadd.ss_family){ case AF_INET: case AF_INET6: -- cgit v1.2.3 From 079baff220a963c365ab8448c421e22e896caaf1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 18 Sep 2019 23:44:36 +0200 Subject: Fix maweb command key handling --- backends/maweb.c | 189 +++++++++++++++++++++++++++++++---------------------- backends/maweb.h | 18 ++++- backends/maweb.md | 76 ++++++++++++++------- backends/winmidi.c | 3 +- 4 files changed, 181 insertions(+), 105 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index c88ca8c..be356d0 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -19,72 +19,57 @@ static uint64_t update_interval = 50; static uint64_t last_update = 0; static uint64_t updates_inflight = 0; -static char* cmdline_keys[] = { - "SET", - "PREV", - "NEXT", - "CLEAR", - "FIXTURE_CHANNEL", - "FIXTURE_GROUP_PRESET", - "EXEC_CUE", - "STORE_UPDATE", - "OOPS", - "ESC", - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "PUNKT", - "PLUS", - "MINUS", - "THRU", - "IF", - "AT", - "FULL", - "HIGH", - "ENTER", - "OFF", - "ON", - "ASSIGN", - "LABEL", - "COPY", - "TIME", - "PAGE", - "MACRO", - "DELETE", - "GOTO", - "GO_PLUS", - "GO_MINUS", - "PAUSE", - "SELECT", - "FIXTURE", - "SEQU", - "CUE", - "PRESET", - "EDIT", - "UPDATE", - "EXEC", - "STORE", - "GROUP", - "PROG_ONLY", - "SPECIAL_DIALOGUE", - "SOLO", - "ODD", - "EVEN", - "WINGS", - "RESET", - "MA", - "layerMode", - "featureSort", - "fixtureSort", - "channelSort", - "hideName" +static maweb_command_key cmdline_keys[] = { + {"PREV", 109, 0, 1}, {"SET", 108, 1, 0, 1}, {"NEXT", 110, 0, 1}, + {"TIME", 58, 1, 1}, {"EDIT", 55, 1, 1}, {"UPDATE", 57, 1, 1}, + {"OOPS", 53, 1, 1}, {"ESC", 54, 1, 1}, {"CLEAR", 105, 1, 1}, + {"0", 86, 1, 1}, {"1", 87, 1, 1}, {"2", 88, 1, 1}, + {"3", 89, 1, 1}, {"4", 90, 1, 1}, {"5", 91, 1, 1}, + {"6", 92, 1, 1}, {"7", 93, 1, 1}, {"8", 94, 1, 1}, + {"9", 95, 1, 1}, {"PUNKT", 98, 1, 1}, {"ENTER", 106, 1, 1}, + {"PLUS", 96, 1, 1}, {"MINUS", 97, 1, 1}, {"THRU", 102, 1, 1}, + {"IF", 103, 1, 1}, {"AT", 104, 1, 1}, {"FULL", 99, 1, 1}, + {"MA", 68, 0, 1}, {"HIGH", 100, 1, 1, 1}, {"SOLO", 101, 1, 1, 1}, + {"SELECT", 42, 1, 1}, {"OFF", 43, 1, 1}, {"ON", 46, 1, 1}, + {"ASSIGN", 63, 1, 1}, {"LABEL", 0, 1, 1}, + {"COPY", 73, 1, 1}, {"DELETE", 69, 1, 1}, {"STORE", 59, 1, 1}, + {"GOTO", 56, 1, 1}, {"PAGE", 70, 1, 1}, {"MACRO", 71, 1, 1}, + {"PRESET", 72, 1, 1}, {"SEQU", 74, 1, 1}, {"CUE", 75, 1, 1}, + {"EXEC", 76, 1, 1}, {"FIXTURE", 83, 1, 1}, {"GROUP", 84, 1, 1}, + {"GO_MINUS", 10, 1, 1}, {"PAUSE", 9, 1, 1}, {"GO_PLUS", 11, 1, 1}, + + {"FIXTURE_CHANNEL", 0, 1, 1}, {"FIXTURE_GROUP_PRESET", 0, 1, 1}, + {"EXEC_CUE", 0, 1, 1}, {"STORE_UPDATE", 0, 1, 1}, {"PROG_ONLY", 0, 1, 1, 1}, + {"SPECIAL_DIALOGUE", 0, 1, 1}, + {"ODD", 0, 1, 1}, {"EVEN", 0, 1, 1}, + {"WINGS", 0, 1, 1}, {"RESET", 0, 1, 1}, + //gma2 internal only + {"CHPGPLUS", 3}, {"CHPGMINUS", 4}, + {"FDPGPLUS", 5}, {"FDPGMINUS", 6}, + {"BTPGPLUS", 7}, {"BTPGMINUS", 8}, + {"X1", 12}, {"X2", 13}, {"X3", 14}, + {"X4", 15}, {"X5", 16}, {"X6", 17}, + {"X7", 18}, {"X8", 19}, {"X9", 20}, + {"X10", 21}, {"X11", 22}, {"X12", 23}, + {"X13", 24}, {"X14", 25}, {"X15", 26}, + {"X16", 27}, {"X17", 28}, {"X18", 29}, + {"X19", 30}, {"X20", 31}, + {"V1", 120}, {"V2", 121}, {"V3", 122}, + {"V4", 123}, {"V5", 124}, {"V6", 125}, + {"V7", 126}, {"V8", 127}, {"V9", 128}, + {"V10", 129}, + {"NIPPLE", 40}, + {"TOOLS", 119}, {"SETUP", 117}, {"BACKUP", 117}, + {"BLIND", 60}, {"FREEZE", 61}, {"PREVIEW", 62}, + {"FIX", 41}, {"TEMP", 44}, {"TOP", 45}, + {"VIEW", 66}, {"EFFECT", 67}, {"CHANNEL", 82}, + {"MOVE", 85}, {"BLACKOUT", 65}, + {"PLEASE", 106}, + {"LIST", 32}, {"USER1", 33}, {"USER2", 34}, + {"ALIGN", 64}, {"HELP", 116}, + {"UP", 107}, {"DOWN", 111}, + {"FASTREVERSE", 47}, {"LEARN", 48}, {"FASTFORWARD", 49}, + {"GO_MINUS_SMALL", 50}, {"PAUSE_SMALL", 51}, {"GO_PLUS_SMALL", 52} }; int init(){ @@ -199,6 +184,22 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){ return 1; #endif } + else if(!strcmp(option, "cmdline")){ + if(!strcmp(value, "console")){ + data->cmdline = cmd_console; + } + else if(!strcmp(value, "remote")){ + data->cmdline = cmd_remote; + } + else if(!strcmp(value, "downgrade")){ + data->cmdline = cmd_downgrade; + } + else{ + fprintf(stderr, "Unknown maweb commandline mode %s for instance %s\n", value, inst->name); + return 1; + } + return 0; + } fprintf(stderr, "Unknown configuration parameter %s for maweb instance %s\n", option, inst->name); return 1; @@ -268,10 +269,15 @@ static channel* maweb_channel(instance* inst, char* spec){ chan.index = strtoul(next_token, NULL, 10); } else{ - for(n = 0; n < sizeof(cmdline_keys) / sizeof(char*); n++){ - //FIXME this is broken for layerMode - if(!strcmp(spec, cmdline_keys[n]) || (*spec == 'l' && !strcmp(spec + 1, cmdline_keys[n]))){ - chan.type = (*spec == 'l') ? cmdline_local : cmdline; + for(n = 0; n < sizeof(cmdline_keys) / sizeof(maweb_command_key); n++){ + if(!strcmp(spec, cmdline_keys[n].name)){ + if((data->cmdline == cmd_remote && !cmdline_keys[n].press && !cmdline_keys[n].release) + || (data->cmdline == cmd_console && !cmdline_keys[n].lua)){ + fprintf(stderr, "maweb cmdline key %s does not work with the current commandline mode for instance %s\n", spec, inst->name); + return NULL; + } + + chan.type = cmdline; chan.index = n + 1; chan.page = 1; break; @@ -599,6 +605,8 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le field = json_obj_str(payload, "appType", NULL); if(!strncmp(field, "dot2", 4)){ data->peer_type = peer_dot2; + //the dot2 can't handle lua commands + data->cmdline = cmd_remote; } else if(!strncmp(field, "gma2", 4)){ data->peer_type = peer_ma2; @@ -860,14 +868,41 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ data->session); break; case cmdline: - snprintf(xmit_buffer, sizeof(xmit_buffer), - "{\"keyname\":\"%s\"," - //"\"autoSubmit\":false," - "\"value\":%d" - "}", cmdline_keys[chan->index], - (v[n].normalised > 0.9) ? 1 : 0); + if(cmdline_keys[chan->index].lua + && (data->cmdline == cmd_console || data->cmdline == cmd_downgrade) + && data->peer_type != peer_dot2){ + //push canbus events + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"command\":\"LUA 'gma.canbus.hardkey(%d, %s, false)'\"," + "\"requestType\":\"command\"," + "\"session\":%" PRIu64 + "}", cmdline_keys[chan->index].lua, + (v[n].normalised > 0.9) ? "true" : "false", + data->session); + } + else if((cmdline_keys[chan->index].press || cmdline_keys[chan->index].release) + && (data->cmdline != cmd_console)){ + //send press/release events if required + if((cmdline_keys[chan->index].press && v[n].normalised > 0.9) + || (cmdline_keys[chan->index].release && v[n].normalised < 0.9)){ + snprintf(xmit_buffer, sizeof(xmit_buffer), + "{\"keyname\":\"%s\"," + "\"autoSubmit\":%s," + "\"value\":%d" + "}", cmdline_keys[chan->index].name, + cmdline_keys[chan->index].auto_submit ? "true" : "null", + (v[n].normalised > 0.9) ? 1 : 0); + } + else{ + continue; + } + } + else{ + fprintf(stderr, "maweb commandline key %s not executed on %s due to mode mismatch\n", + cmdline_keys[chan->index].name, inst->name); + continue; + } break; - //TODO cmdline_local default: fprintf(stderr, "maweb control not yet implemented\n"); return 1; diff --git a/backends/maweb.h b/backends/maweb.h index 3738367..b9de68f 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -25,8 +25,7 @@ typedef enum /*_maweb_channel_type*/ { exec_button = 2, //gma: 0 dot: 0 exec_lower = 3, //gma: 1 dot: 1 exec_upper = 4, //gma: 2 dot: 0 - cmdline, - cmdline_local + cmdline } maweb_channel_type; typedef enum /*_maweb_peer_type*/ { @@ -43,6 +42,12 @@ typedef enum /*_ws_conn_state*/ { ws_closed } maweb_state; +typedef enum /*_maweb_cmdline_mode*/ { + cmd_remote = 0, + cmd_console, + cmd_downgrade +} maweb_cmdline_mode; + typedef enum /*_ws_frame_op*/ { ws_text = 1, ws_binary = 2, @@ -50,6 +55,14 @@ typedef enum /*_ws_frame_op*/ { ws_pong = 10 } maweb_operation; +typedef struct { + char* name; + unsigned lua; + uint8_t press; + uint8_t release; + uint8_t auto_submit; +} maweb_command_key; + typedef struct /*_maweb_channel*/ { maweb_channel_type type; uint16_t page; @@ -73,6 +86,7 @@ typedef struct /*_maweb_instance_data*/ { size_t channels; maweb_channel_data* channel; + maweb_cmdline_mode cmdline; int fd; maweb_state state; diff --git a/backends/maweb.md b/backends/maweb.md index 93cd776..a18cde4 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -26,6 +26,14 @@ Web Remote. Set a web remote password using the option below the activation sett | `host` | `10.23.42.21 80` | none | Host address (and optional port) of the MA Web Remote | | `user` | `midimonster` | none | User for the remote session (GrandMA2) | | `password` | `midimonster` | `midimonster` | Password for the remote session | +| `cmdline` | `console` | `remote` | Commandline key handling mode (see below) | + +The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles +command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are +not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects the +key events into the main console's command line. When connected to a dot2 console, the use of commandline keys +will not be possible. With the `downgrade` mode, keys are handled on the console if possible, falling back to +remote handling if not. #### Channel specification @@ -33,7 +41,7 @@ Currently, three types of MA controls can be assigned, with each having some sub * Fader executor * Button executor -* Command line buttons +* Command keys ##### Executors @@ -70,33 +78,52 @@ mw1.page2.button103 > mw1.page3.fader101 mw1.page2.button803 > mw1.page3.button516 ``` -##### Command line buttons +##### Command keys -Command line buttons will be pressed when the incoming event value is greater than `0.9` and released when it is less than that. +Command keys will be pressed when the incoming event value is greater than `0.9` and released when it is less than that. They can be mapped using the syntax ``` -mw1. +mw1. ``` -The following button names are recognized by the backend: - -| Supported | Command | Line | Keys | | | | -|---------------|---------------|---------------|---------------|-----------------------|-------------------------------|---------------| -| `SET` | `PREV` | `NEXT` | `CLEAR` | `FIXTURE_CHANNEL` | `FIXTURE_GROUP_PRESET` | `EXEC_CUE` | -| `STORE_UPDATE`| `OOPS` | `ESC` | `OFF` | `ON` | `MA` | `STORE` | -| `0` | `1` | `2` | `3` | `4` | `5` | `6` | -| `7` | `8` | `9` | `PUNKT` | `PLUS` | `MINUS` | `THRU` | -| `IF` | `AT` | `FULL` | `HIGH` | `ENTER` | `ASSIGN` | `LABEL` | -| `COPY` | `TIME` | `PAGE` | `MACRO` | `DELETE` | `GOTO` | `GO_PLUS` | -| `GO_MINUS` | `PAUSE` | `SELECT` | `FIXTURE` | `SEQU` | `CUE` | `PRESET` | -| `EDIT` | `UPDATE` | `EXEC` | `GROUP` | `PROG_ONLY` | `SPECIAL_DIALOGUE` | `SOLO` | -| `ODD` | `EVEN` | `WINGS` | `RESET` | `layerMode` | `featureSort` | `fixtureSort` | -| `channelSort` | `hideName` | | | | | | - -Note that each Web Remote connection has it's own command line, as such commands entered using this backend will not affect -the command line on the main console. To do that, you will need to use another backend to feed input to the MA, such as -the ArtNet or MIDI backends. +The following keys are mappable in all commandline modes and work on all consoles + +| Supported | Command | Line | Keys | | | +|---------------|---------------|---------------|---------------|---------------|---------------| +| `PREV` | `SET` | `NEXT` | `TIME` | `EDIT` | `UPDATE` | +| `OOPS` | `ESC` | `CLEAR` | `0` | `1` | `2` | +| `3` | `4` | `5` | `6` | `7` | `8` | +| `9` | `PUNKT` | `ENTER` | `PLUS` | `MINUS` | `THRU` | +| `IF` | `AT` | `FULL` | `MA` | `HIGH` | `SOLO` | +| `SELECT` | `OFF` | `ON` | `ASSIGN` | `COPY` | `DELETE` | +| `STORE` | `GOTO` | `PAGE` | `MACRO` | `PRESET` | `SEQU` | +| `CUE` | `EXEC` | `FIXTURE` | `GROUP` | `GO_MINUS` | `PAUSE` | +| `GO_PLUS` | | | | | | + +The following keys only work in the `remote` or `downgrade` commandline mode, but on all consoles + +| Web | Remote | specific | | | +|---------------|-----------------------|-------------------------------|---------------|-----------------------| +| `LABEL` |`FIXTURE_CHANNEL` | `FIXTURE_GROUP_PRESET` | `EXEC_CUE` | `STORE_UPDATE` | +| `PROG_ONLY` | `SPECIAL_DIALOGUE` | `ODD` | `EVEN` | `WINGS` | +| `RESET` | | | | | + +The following keys only work in the `console` or `downgrade` command line modes on a GrandMA2 + +| GrandMA2 | console | only | | | | +|---------------|---------------|---------------|---------------|---------------|---------------| +| `CHPGPLUS` | `CHPGMINUS` | `FDPGPLUS` | `FDPGMINUS` | `BTPGPLUS` | `BTPGMINUS` | +| `X1` | `X2` | `X3` | `X4` | `X5` | `X6` | +| `X7` | `X8` | `X9` | `X10` | `X11` | `X12` | +| `X13` | `X14` | `X15` | `X16` | `X17` | `X18` | +| `X19` | `X20` | `V1` | `V2` | `V3` | `V4` | +| `V5` | `V6` | `V7` | `V8` | `V9` | `V10` | +| `NIPPLE` | `TOOLS` | `SETUP` | `BACKUP` | `BLIND` | `FREEZE` | +| `PREVIEW` | `FIX` | `TEMP` | `TOP` | `VIEW` | `EFFECT` | +| `CHANNEL` | `MOVE` | `BLACKOUT` | `PLEASE` | `LIST` | `USER1` | +| `USER2` | `ALIGN` | `HELP` | `UP` | `DOWN` | `FASTREVERSE` | +| `LEARN` | `FASTFORWARD` | `GO_MINUS_SMALL` | `PAUSE_SMALL` | `GO_PLUS_SMALL` | | #### Known bugs / problems @@ -109,6 +136,5 @@ Data input from the console is done by actively querying the state of all mapped at low latency. A lower input interval value will produce data with lower latency, at the cost of network & CPU usage. Higher values will make the input "step" more, but will not consume as many CPU cycles and network bandwidth. -When requesting button executor events on the fader pages (execs 101 to 222) of a dot2 console, map at least one fader control from the 0 - 22 range or input will not work due to strange limitations in the MA Web API. - -Command line events are sent, but I'm not sure they're being handled yet. +When requesting button executor events on the fader pages (execs 101 to 222) of a dot2 console, map at least one fader control from the 0 - 22 range +or input will not work due to strange limitations in the MA Web API. diff --git a/backends/winmidi.c b/backends/winmidi.c index 67187ac..13c4b4a 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -21,6 +21,7 @@ static struct { }; //TODO detect option +//TODO receive feedback socket until EAGAIN int init(){ backend winmidi = { @@ -465,7 +466,7 @@ static int winmidi_start(){ } //open the feedback sockets - //for some reason this while construct fails to work on 'real' windows with ipv6 + //for some reason the feedback connection fails to work on 'real' windows with ipv6 backend_config.socket_pair[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); if(backend_config.socket_pair[0] < 0){ fprintf(stderr, "winmidi failed to open feedback socket\n"); -- cgit v1.2.3 From fe4a7b538b9b6c53299d883bee258e4d3597be9d Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 20 Sep 2019 03:43:13 +0200 Subject: Update documentation, fix minor leaks --- backends/maweb.c | 1 - backends/maweb.md | 13 +++++++------ backends/midi.c | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index be356d0..450c388 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -121,7 +121,6 @@ static int channel_comparator(const void* raw_a, const void* raw_b){ return a->index - b->index; } return a->type - b->type; - } //if either one is not an exec, sort by type first, index second if(a->type != b->type){ diff --git a/backends/maweb.md b/backends/maweb.md index a18cde4..5b996a9 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -2,7 +2,8 @@ This backend connects directly with the integrated *MA Web Remote* of MA Lighting consoles and OnPC instances (GrandMA2 / GrandMA2 OnPC / GrandMA Dot2 / GrandMA Dot2 OnPC). -It grants read-write access to the console's playback controls as well as write access to the command line. +It grants read-write access to the console's playback controls as well as write access to most command +line and control keys. #### Setting up the console @@ -30,10 +31,10 @@ Web Remote. Set a web remote password using the option below the activation sett The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are -not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects the -key events into the main console's command line. When connected to a dot2 console, the use of commandline keys -will not be possible. With the `downgrade` mode, keys are handled on the console if possible, falling back to -remote handling if not. +not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects key events +into the main console. This mode also supports additional hardkeys that are only available on GrandMA consoles. +When connected to a dot2 console while this mode is active, the use of commandline keys will not be possible. +With the `downgrade` mode, keys are handled on the console if possible, falling back to remote handling if not. #### Channel specification @@ -101,7 +102,7 @@ The following keys are mappable in all commandline modes and work on all console | `CUE` | `EXEC` | `FIXTURE` | `GROUP` | `GO_MINUS` | `PAUSE` | | `GO_PLUS` | | | | | | -The following keys only work in the `remote` or `downgrade` commandline mode, but on all consoles +The following keys only work when keys are being handled with a virtual command line | Web | Remote | specific | | | |---------------|-----------------------|-------------------------------|---------------|-----------------------| diff --git a/backends/midi.c b/backends/midi.c index 65ce48a..d9ac698 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -358,7 +358,7 @@ static int midi_start(){ //connect to the sequencer if(snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0){ fprintf(stderr, "Failed to open ALSA sequencer\n"); - return 0; + goto bail; } snd_seq_nonblock(sequencer, 1); @@ -367,7 +367,7 @@ static int midi_start(){ //update the sequencer client name if(snd_seq_set_client_name(sequencer, sequencer_name ? sequencer_name : "MIDIMonster") < 0){ fprintf(stderr, "Failed to set MIDI client name to %s\n", sequencer_name); - return 1; + goto bail; } //create all ports -- cgit v1.2.3 From e556b1719a8906c14d329eb3c08574428cad0aae Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 20 Sep 2019 17:58:27 +0200 Subject: Fix maweb channel ordering --- backends/maweb.c | 14 ++++++++++++-- backends/maweb.h | 4 ++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 450c388..baa516a 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -5,6 +5,7 @@ #include #endif +#define DEBUG #include "libmmbackend.h" #include "maweb.h" @@ -235,6 +236,7 @@ static channel* maweb_channel(instance* inst, char* spec){ 0 }; char* next_token = NULL; + channel* channel_ref = NULL; size_t n; if(!strncmp(spec, "page", 4)){ @@ -299,7 +301,9 @@ static channel* maweb_channel(instance* inst, char* spec){ data->channels++; } - return mm_channel(inst, maweb_channel_index(data, chan.type, chan.page, chan.index), 1); + channel_ref = mm_channel(inst, maweb_channel_index(data, chan.type, chan.page, chan.index), 1); + data->channel[maweb_channel_index(data, chan.type, chan.page, chan.index)].chan = channel_ref; + return channel_ref; } fprintf(stderr, "Failed to parse maweb channel spec %s\n", spec); @@ -906,6 +910,7 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ fprintf(stderr, "maweb control not yet implemented\n"); return 1; } + DBGPF("maweb command out %s\n", xmit_buffer); maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } return 0; @@ -982,7 +987,7 @@ static int maweb_handle(size_t num, managed_fd* fds){ } static int maweb_start(){ - size_t n, u; + size_t n, u, p; instance** inst = NULL; maweb_instance_data* data = NULL; @@ -997,6 +1002,11 @@ static int maweb_start(){ data = (maweb_instance_data*) inst[u]->impl; qsort(data->channel, data->channels, sizeof(maweb_channel_data), channel_comparator); + //re-set channel identifiers + for(p = 0; p < data->channels; p++){ + data->channel[p].chan->ident = p; + } + if(maweb_connect(inst[u])){ fprintf(stderr, "Failed to open connection to MA Web Remote for instance %s\n", inst[u]->name); free(inst); diff --git a/backends/maweb.h b/backends/maweb.h index b9de68f..14e4755 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -72,6 +72,10 @@ typedef struct /*_maweb_channel*/ { double in; double out; + + //reverse reference required because the identifiers are not stable + //because we sort the backing store... + channel* chan; } maweb_channel_data; typedef struct /*_maweb_instance_data*/ { -- cgit v1.2.3 From 65bd41387c8dbf67812de1881198a47c9bb4b55e Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 5 Oct 2019 19:30:22 +0200 Subject: Implement detect option for winmidi --- backends/maweb.c | 7 ++++--- backends/midi.c | 3 +-- backends/winmidi.c | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index baa516a..c98d04b 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -5,7 +5,6 @@ #include #endif -#define DEBUG #include "libmmbackend.h" #include "maweb.h" @@ -891,10 +890,12 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"keyname\":\"%s\"," "\"autoSubmit\":%s," - "\"value\":%d" + "\"value\":%d," + "\"session\":%" PRIu64 "}", cmdline_keys[chan->index].name, cmdline_keys[chan->index].auto_submit ? "true" : "null", - (v[n].normalised > 0.9) ? 1 : 0); + (v[n].normalised > 0.9) ? 1 : 0, + data->session); } else{ continue; diff --git a/backends/midi.c b/backends/midi.c index d9ac698..536457d 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -56,8 +56,7 @@ static int midi_configure(char* option, char* value){ sequencer_name = strdup(value); return 0; } - - if(!strcmp(option, "detect")){ + else if(!strcmp(option, "detect")){ midi_config.detect = 1; if(!strcmp(value, "off")){ midi_config.detect = 0; diff --git a/backends/winmidi.c b/backends/winmidi.c index 13c4b4a..83d0c7e 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -9,6 +9,7 @@ static struct { uint8_t list_devices; + uint8_t detect; int socket_pair[2]; CRITICAL_SECTION push_events; @@ -60,6 +61,13 @@ static int winmidi_configure(char* option, char* value){ } return 0; } + else if(!strcmp(option, "detect")){ + backend_config.detect = 0; + if(!strcmp(value, "on")){ + backend_config.detect = 1; + } + return 0; + } fprintf(stderr, "Unknown winmidi backend option %s\n", option); return 1; @@ -232,6 +240,22 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v return 0; } +static char* winmidi_type_name(uint8_t typecode){ + switch(typecode){ + case note: + return "note"; + case cc: + return "cc"; + case pressure: + return "pressure"; + case aftertouch: + return "aftertouch"; + case pitchbend: + return "pitch"; + } + return "unknown"; +} + static int winmidi_handle(size_t num, managed_fd* fds){ size_t u; ssize_t bytes = 0; @@ -249,6 +273,23 @@ static int winmidi_handle(size_t num, managed_fd* fds){ //push queued events EnterCriticalSection(&backend_config.push_events); for(u = 0; u < backend_config.events_active; u++){ + if(backend_config.detect){ + //pretty-print channel-wide events + if(backend_config.event[u].channel.fields.type == pitchbend + || backend_config.event[u].channel.fields.type == aftertouch){ + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s\n", + backend_config.event[u].inst->name, + backend_config.event[u].channel.fields.channel, + winmidi_type_name(backend_config.event[u].channel.fields.type)); + } + else{ + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d\n", + backend_config.event[u].inst->name, + backend_config.event[u].channel.fields.channel, + winmidi_type_name(backend_config.event[u].channel.fields.type), + backend_config.event[u].channel.fields.control); + } + } chan = mm_channel(backend_config.event[u].inst, backend_config.event[u].channel.label, 0); if(chan){ mm_channel_event(chan, backend_config.event[u].value); -- cgit v1.2.3 From 24e5594c754ec74918848d33d513db69d54aba47 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 3 Nov 2019 16:59:13 +0100 Subject: Fix winmidi wire format --- backends/lua.c | 3 --- backends/winmidi.c | 12 ++++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index 365cf3e..1cd965e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -456,9 +456,6 @@ static int lua_start(){ } free(inst); - if(!n){ - return 0; - } #ifdef MMBACKEND_LUA_TIMERFD //register the timer with the core diff --git a/backends/winmidi.c b/backends/winmidi.c index 83d0c7e..de7d867 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -21,7 +21,6 @@ static struct { .socket_pair = {-1, -1} }; -//TODO detect option //TODO receive feedback socket until EAGAIN int init(){ @@ -123,8 +122,9 @@ static channel* winmidi_channel(instance* inst, char* spec){ next_token = spec + 7; } } - else{ - fprintf(stderr, "Unknown winmidi channel specification %s\n", spec); + + if(!next_token){ + fprintf(stderr, "Invalid winmidi channel specification %s\n", spec); return NULL; } @@ -226,8 +226,8 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v break; case pitchbend: output.components.status = 0xE0 | ident.fields.channel; - output.components.data1 = ((int)(v[u].normalised * 32639.0)) & 0xFF; - output.components.data2 = (((int)(v[u].normalised * 32639.0)) & 0xFF00) >> 8; + output.components.data1 = ((int)(v[u].normalised * 16384.0)) & 0x7F; + output.components.data2 = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; break; default: fprintf(stderr, "Unknown winmidi channel type %d\n", ident.fields.type); @@ -357,7 +357,7 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW case 0xE0: ident.fields.type = pitchbend; ident.fields.control = 0; - val.normalised = (double)((input.components.data2 << 8) | input.components.data1) / 32639.0; + val.normalised = (double)((input.components.data2 << 7) | input.components.data1) / 16384.0; break; default: fprintf(stderr, "winmidi unhandled status byte %02X\n", input.components.status); -- cgit v1.2.3 From ff587cb77ee4a7e9169affbfefd84547da6fea38 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 18:50:57 +0100 Subject: Implement JACK backend --- README.md | 7 +- backends/Makefile | 3 +- backends/jack.c | 742 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/jack.h | 76 ++++++ backends/jack.md | 84 +++++++ 5 files changed, 909 insertions(+), 3 deletions(-) create mode 100644 backends/jack.c create mode 100644 backends/jack.h create mode 100644 backends/jack.md diff --git a/README.md b/README.md index 3cdac99..f23e696 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ Currently, the MIDIMonster supports the following protocols: | Protocol | Operating Systems | Notes | Backends | |-------------------------------|-----------------------|-------------------------------|-------------------------------| -| MIDI | Linux, Windows | Linux: via ALSA | [`midi`](backends/midi.md), [`winmidi`](backends/winmidi.md) | +| MIDI | Linux, Windows | Linux: via ALSA/JACK | [`midi`](backends/midi.md), [`winmidi`](backends/winmidi.md), [`jack`](backends/jack.md) | | 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) | | evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | | Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | | MA Lighting Web Remote | Linux, Windows, OSX | GrandMA and dot2 (incl. OnPC) | [`maweb`](backends/maweb.md) | +| JACK/LV2 Control Voltage (CV) | Linux | | [`jack`](backends/jack.md) | with additional flexibility provided by a [Lua scripting environment](backends/lua.md). @@ -117,6 +118,7 @@ configuration options, channel specification syntax and any known problems or ot special information. These documentation files are located in the `backends/` directory. * [`midi` backend documentation](backends/midi.md) +* [`jack` backend documentation](backends/jack.md) * [`winmidi` backend documentation](backends/winmidi.md) * [`artnet` backend documentation](backends/artnet.md) * [`sacn` backend documentation](backends/sacn.md) @@ -137,10 +139,11 @@ This section will explain how to build the provided sources to be able to run In order to build the MIDIMonster, you'll need some libraries that provide support for the protocols to translate. -* `libasound2-dev` (for the MIDI backend) +* `libasound2-dev` (for the ALSA MIDI backend) * `libevdev-dev` (for the evdev backend) * `liblua5.3-dev` (for the lua backend) * `libola-dev` (for the optional OLA backend) +* `libjack-jackd2-dev` (for the JACK backend) * `pkg-config` (as some projects and systems like to spread their files around) * `libssl-dev` (for the MA Web Remote backend) * A C compiler diff --git a/backends/Makefile b/backends/Makefile index 293b434..c5755c9 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,5 +1,5 @@ .PHONY: all clean full -LINUX_BACKENDS = midi.so evdev.so +LINUX_BACKENDS = midi.so evdev.so jack.so WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so OPTIONAL_BACKENDS = ola.so @@ -41,6 +41,7 @@ maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) winmidi.dll: LDLIBS += -lwinmm -lws2_32 +jack.so: LDLIBS = -ljack -lpthread midi.so: LDLIBS = -lasound evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) diff --git a/backends/jack.c b/backends/jack.c new file mode 100644 index 0000000..5a88cf2 --- /dev/null +++ b/backends/jack.c @@ -0,0 +1,742 @@ +#include +#include +#include +#include +#include + +#include "jack.h" +#include +#include + +#define BACKEND_NAME "jack" +#define JACKEY_SIGNAL_TYPE "http://jackaudio.org/metadata/signal-type" + +//FIXME pitchbend range is somewhat oob + +static struct /*_mmjack_backend_cfg*/ { + unsigned verbosity; + volatile sig_atomic_t jack_shutdown; +} config = { + .verbosity = 1, + .jack_shutdown = 0 +}; + +int init(){ + backend mmjack = { + .name = BACKEND_NAME, + .conf = mmjack_configure, + .create = mmjack_instance, + .conf_instance = mmjack_configure_instance, + .channel = mmjack_channel, + .handle = mmjack_set, + .process = mmjack_handle, + .start = mmjack_start, + .shutdown = mmjack_shutdown + }; + + if(sizeof(mmjack_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "jack channel identification union out of bounds\n"); + return 1; + } + + //register backend + if(mm_backend_register(mmjack)){ + fprintf(stderr, "Failed to register jack backend\n"); + return 1; + } + return 0; +} + +static void mmjack_message_print(const char* msg){ + fprintf(stderr, "JACK message: %s\n", msg); +} + +static void mmjack_message_ignore(const char* msg){ +} + +static int mmjack_midiqueue_append(mmjack_port* port, mmjack_channel_ident ident, uint16_t value){ + //append events + if(port->queue_len == port->queue_alloc){ + //extend the queue + port->queue = realloc(port->queue, (port->queue_len + JACK_MIDIQUEUE_CHUNK) * sizeof(mmjack_midiqueue)); + if(!port->queue){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + port->queue_alloc += JACK_MIDIQUEUE_CHUNK; + } + + port->queue[port->queue_len].ident.label = ident.label; + port->queue[port->queue_len].raw = value; + port->queue_len++; + DBGPF("Appended event to queue for %s, now at %" PRIsize_t " entries\n", port->name, port->queue_len); + return 0; +} + +static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){ + void* buffer = jack_port_get_buffer(port->port, nframes); + jack_nframes_t event_count = jack_midi_get_event_count(buffer); + jack_midi_event_t event; + jack_midi_data_t* event_data; + mmjack_channel_ident ident; + size_t u; + uint16_t value; + + if(port->input){ + if(event_count){ + DBGPF("Reading %u MIDI events from jack port %s\n", event_count, port->name); + for(u = 0; u < event_count; u++){ + ident.label = 0; + //read midi data from stream + jack_midi_event_get(&event, buffer, u); + //ident.fields.port set on output in mmjack_handle_midi + ident.fields.sub_channel = event.buffer[0] & 0x0F; + ident.fields.sub_type = event.buffer[0] & 0xF0; + if(ident.fields.sub_type == 0x80){ + ident.fields.sub_type = midi_note; + value = 0; + } + else if(ident.fields.sub_type == midi_pitchbend){ + value = event.buffer[1] | (event.buffer[2] << 7); + } + else if(ident.fields.sub_type == midi_aftertouch){ + value = event.buffer[1]; + } + else{ + ident.fields.sub_control = event.buffer[1]; + value = event.buffer[2]; + } + //append midi data + mmjack_midiqueue_append(port, ident, value); + } + port->mark = 1; + *mark = 1; + } + } + else{ + //clear buffer + jack_midi_clear_buffer(buffer); + + for(u = 0; u < port->queue_len; u++){ + //build midi event + ident.label = port->queue[u].ident.label; + event_data = jack_midi_event_reserve(buffer, u, (ident.fields.sub_type == midi_aftertouch) ? 2 : 3); + if(!event_data){ + fprintf(stderr, "Failed to reserve MIDI stream data\n"); + return 1; + } + event_data[0] = ident.fields.sub_channel | ident.fields.sub_type; + if(ident.fields.sub_type == midi_pitchbend){ + event_data[1] = port->queue[u].raw & 0x7F; + event_data[2] = (port->queue[u].raw >> 7) & 0x7F; + } + else if(ident.fields.sub_type == midi_aftertouch){ + event_data[1] = port->queue[u].raw & 0x7F; + } + else{ + event_data[1] = ident.fields.sub_control; + event_data[2] = port->queue[u].raw & 0x7F; + } + } + + if(port->queue_len){ + DBGPF("Wrote %" PRIsize_t " MIDI events to jack port %s\n", port->queue_len, port->name); + } + port->queue_len = 0; + } + return 0; +} + +static int mmjack_process_cv(instance* inst, mmjack_port* port, size_t nframes, size_t* mark){ + jack_default_audio_sample_t* audio_buffer = jack_port_get_buffer(port->port, nframes); + size_t u; + + if(port->input){ + //read updated data into the local buffer + //FIXME maybe we dont want to always use the first sample... + if((double) audio_buffer[0] != port->last){ + port->last = audio_buffer[0]; + port->mark = 1; + *mark = 1; + } + } + else{ + for(u = 0; u < nframes; u++){ + audio_buffer[u] = port->last; + } + } + return 0; +} + +static int mmjack_process(jack_nframes_t nframes, void* instp){ + instance* inst = (instance*) instp; + mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; + size_t p, mark = 0; + int rv = 0; + + //DBGPF("jack callback for %d frames on %s\n", nframes, inst->name); + + for(p = 0; p < data->ports; p++){ + pthread_mutex_lock(&data->port[p].lock); + switch(data->port[p].type){ + case port_midi: + //DBGPF("Handling MIDI port %s.%s\n", inst->name, data->port[p].name); + rv |= mmjack_process_midi(inst, data->port + p, nframes, &mark); + break; + case port_cv: + //DBGPF("Handling CV port %s.%s\n", inst->name, data->port[p].name); + rv |= mmjack_process_cv(inst, data->port + p, nframes, &mark); + break; + default: + fprintf(stderr, "Unhandled jack port type in processing callback\n"); + pthread_mutex_unlock(&data->port[p].lock); + return 1; + } + pthread_mutex_unlock(&data->port[p].lock); + } + + //notify the main thread + if(mark){ + DBGPF("Notifying handler thread for jack instance %s\n", inst->name); + send(data->fd, "c", 1, 0); + } + return rv; +} + +static void mmjack_server_shutdown(void* inst){ + fprintf(stderr, "jack server shutdown notification\n"); + config.jack_shutdown = 1; +} + +static int mmjack_configure(char* option, char* value){ + if(!strcmp(option, "debug")){ + if(!strcmp(value, "on")){ + config.verbosity |= 2; + return 0; + } + config.verbosity &= ~2; + return 0; + } + if(!strcmp(option, "errors")){ + if(!strcmp(value, "on")){ + config.verbosity |= 1; + return 0; + } + config.verbosity &= ~1; + return 0; + } + + fprintf(stderr, "Unknown jack backend option %s\n", option); + return 1; +} + +static int mmjack_parse_portconfig(mmjack_port* port, char* spec){ + char* token = NULL; + + for(token = strtok(spec, " "); token; token = strtok(NULL, " ")){ + if(!strcmp(token, "in")){ + port->input = 1; + } + else if(!strcmp(token, "out")){ + port->input = 0; + } + else if(!strcmp(token, "midi")){ + port->type = port_midi; + } + else if(!strcmp(token, "osc")){ + port->type = port_osc; + } + else if(!strcmp(token, "cv")){ + port->type = port_cv; + } + else if(!strcmp(token, "max")){ + token = strtok(NULL, " "); + if(!token){ + fprintf(stderr, "jack port %s configuration missing argument\n", port->name); + return 1; + } + port->max = strtod(token, NULL); + } + else if(!strcmp(token, "min")){ + token = strtok(NULL, " "); + if(!token){ + fprintf(stderr, "jack port %s configuration missing argument\n", port->name); + return 1; + } + port->min = strtod(token, NULL); + } + else{ + fprintf(stderr, "Unknown jack channel configuration token %s on port %s\n", token, port->name); + return 1; + } + } + + if(port->type == port_none){ + fprintf(stderr, "jack channel %s assigned no port type\n", port->name); + return 1; + } + return 0; +} + +static int mmjack_configure_instance(instance* inst, char* option, char* value){ + mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; + size_t p; + + if(!strcmp(option, "name")){ + if(data->client_name){ + free(data->client_name); + } + data->client_name = strdup(value); + return 0; + } + else if(!strcmp(option, "server")){ + if(data->server_name){ + free(data->server_name); + } + data->server_name = strdup(value); + return 0; + } + + //register new port, first check for unique name + for(p = 0; p < data->ports; p++){ + if(!strcmp(data->port[p].name, option)){ + fprintf(stderr, "jack instance %s has duplicate port %s\n", inst->name, option); + return 1; + } + } + if(strchr(option, '.')){ + fprintf(stderr, "Invalid jack channel spec %s.%s\n", inst->name, option); + } + + //add port to registry + //TODO for OSC ports we need to configure subchannels for each message + data->port = realloc(data->port, (data->ports + 1) * sizeof(mmjack_port)); + if(!data->port){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + data->port[data->ports].name = strdup(option); + if(!data->port[data->ports].name){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + if(mmjack_parse_portconfig(data->port + p, value)){ + return 1; + } + data->ports++; + return 0; +} + +static instance* mmjack_instance(){ + instance* inst = mm_instance(); + if(!inst){ + return NULL; + } + + inst->impl = calloc(1, sizeof(mmjack_instance_data)); + if(!inst->impl){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + return inst; +} + +static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){ + char* next_token = NULL; + + if(!strncmp(spec, "ch", 2)){ + next_token = spec + 2; + if(!strncmp(spec, "channel", 7)){ + next_token = spec + 7; + } + } + + if(!next_token){ + fprintf(stderr, "Invalid jack MIDI spec %s\n", spec); + return 1; + } + + ident->fields.sub_channel = strtoul(next_token, &next_token, 10); + if(ident->fields.sub_channel > 15){ + fprintf(stderr, "Invalid jack MIDI spec %s, channel out of range\n", spec); + return 1; + } + + if(*next_token != '.'){ + fprintf(stderr, "Invalid jack MIDI spec %s\n", spec); + return 1; + } + + next_token++; + + if(!strncmp(next_token, "cc", 2)){ + ident->fields.sub_type = midi_cc; + next_token += 2; + } + else if(!strncmp(next_token, "note", 4)){ + ident->fields.sub_type = midi_note; + next_token += 4; + } + else if(!strncmp(next_token, "pressure", 8)){ + ident->fields.sub_type = midi_pressure; + next_token += 8; + } + else if(!strncmp(next_token, "pitch", 5)){ + ident->fields.sub_type = midi_pitchbend; + } + else if(!strncmp(next_token, "aftertouch", 10)){ + ident->fields.sub_type = midi_aftertouch; + } + else{ + fprintf(stderr, "Unknown jack MIDI control type in spec %s\n", spec); + return 1; + } + + ident->fields.sub_control = strtoul(next_token, NULL, 10); + + if(ident->fields.sub_type == midi_none + || ident->fields.sub_control > 127){ + fprintf(stderr, "Invalid jack MIDI spec %s\n", spec); + return 1; + } + return 0; +} + +static channel* mmjack_channel(instance* inst, char* spec){ + mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; + mmjack_channel_ident ident = { + .label = 0 + }; + size_t u; + + for(u = 0; u < data->ports; u++){ + if(!strncmp(spec, data->port[u].name, strlen(data->port[u].name)) + && (spec[strlen(data->port[u].name)] == '.' || spec[strlen(data->port[u].name)] == 0)){ + ident.fields.port = u; + break; + } + } + + if(u == data->ports){ + fprintf(stderr, "jack port %s.%s not found\n", inst->name, spec); + return NULL; + } + + if(data->port[u].type == port_midi){ + //parse midi subspec + if(!spec[strlen(data->port[u].name)] + || mmjack_parse_midispec(&ident, spec + strlen(data->port[u].name) + 1)){ + return NULL; + } + } + else if(data->port[u].type == port_osc){ + //TODO parse osc subspec + } + + return mm_channel(inst, ident.label, 1); +} + +static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v){ + mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; + mmjack_channel_ident ident = { + .label = 0 + }; + size_t u; + double range; + uint16_t value; + + for(u = 0; u < num; u++){ + ident.label = c[u]->ident; + + if(data->port[ident.fields.port].input){ + fprintf(stderr, "jack port %s.%s is an input port, no output is possible\n", inst->name, data->port[ident.fields.port].name); + continue; + } + range = data->port[ident.fields.port].max - data->port[ident.fields.port].min; + + pthread_mutex_lock(&data->port[ident.fields.port].lock); + switch(data->port[ident.fields.port].type){ + case port_cv: + //scale value to given range + data->port[ident.fields.port].last = (range * v[u].normalised) + data->port[ident.fields.port].min; + DBGPF("CV port %s updated to %f\n", data->port[ident.fields.port].name, data->port[ident.fields.port].last); + break; + case port_midi: + value = v[u].normalised * 127.0; + if(ident.fields.sub_type == midi_pitchbend){ + value = ((uint16_t)(v[u].normalised * 16384.0)); + } + if(mmjack_midiqueue_append(data->port + ident.fields.port, ident, value)){ + pthread_mutex_unlock(&data->port[ident.fields.port].lock); + return 1; + } + break; + default: + fprintf(stderr, "No handler implemented for jack port type %s.%s\n", inst->name, data->port[ident.fields.port].name); + break; + } + pthread_mutex_unlock(&data->port[ident.fields.port].lock); + } + + return 0; +} + +static void mmjack_handle_midi(instance* inst, size_t index, mmjack_port* port){ + size_t u; + channel* chan = NULL; + channel_value val; + + for(u = 0; u < port->queue_len; u++){ + port->queue[u].ident.fields.port = index; + chan = mm_channel(inst, port->queue[u].ident.label, 0); + if(chan){ + if(port->queue[u].ident.fields.sub_type == midi_pitchbend){ + val.normalised = ((double)port->queue[u].raw) / 16384.0; + } + else{ + val.normalised = ((double)port->queue[u].raw) / 127.0; + } + DBGPF("Pushing MIDI channel %d type %02X control %d value %f raw %d label %" PRIu64 "\n", + port->queue[u].ident.fields.sub_channel, + port->queue[u].ident.fields.sub_type, + port->queue[u].ident.fields.sub_control, + val.normalised, + port->queue[u].raw, + port->queue[u].ident.label); + if(mm_channel_event(chan, val)){ + fprintf(stderr, "Failed to push MIDI event to core on jack port %s.%s\n", inst->name, port->name); + } + } + } + + if(port->queue_len){ + DBGPF("Pushed %" PRIsize_t " MIDI events to core for jack port %s.%s\n", port->queue_len, inst->name, port->name); + } + port->queue_len = 0; +} + +static void mmjack_handle_cv(instance* inst, size_t index, mmjack_port* port){ + mmjack_channel_ident ident = { + .fields.port = index + }; + double range; + channel_value val; + + channel* chan = mm_channel(inst, ident.label, 0); + if(!chan){ + //this might happen if a channel is registered but not mapped + DBGPF("Failed to match jack CV channel %s.%s to core channel\n", inst->name, port->name); + return; + } + + //normalize value + range = port->max - port->min; + val.normalised = port->last - port->min; + val.normalised /= range; + val.normalised = clamp(val.normalised, 1.0, 0.0); + DBGPF("Pushing CV channel %s value %f raw %f min %f max %f\n", port->name, val.normalised, port->last, port->min, port->max); + if(mm_channel_event(chan, val)){ + fprintf(stderr, "Failed to push CV event to core for %s.%s\n", inst->name, port->name); + } +} + +static int mmjack_handle(size_t num, managed_fd* fds){ + size_t u, p; + instance* inst = NULL; + mmjack_instance_data* data = NULL; + ssize_t bytes; + uint8_t recv_buf[1024]; + + if(num){ + for(u = 0; u < num; u++){ + bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); + if(bytes < 0){ + fprintf(stderr, "Failed to receive on feedback socket for instance %s\n", inst->name); + return 1; + } + inst = (instance*) fds[u].impl; + data = (mmjack_instance_data*) inst->impl; + + for(p = 0; p < data->ports; p++){ + if(data->port[p].input && data->port[p].mark){ + pthread_mutex_lock(&data->port[p].lock); + switch(data->port[p].type){ + case port_cv: + mmjack_handle_cv(inst, p, data->port + p); + break; + case port_midi: + mmjack_handle_midi(inst, p, data->port + p); + break; + default: + fprintf(stderr, "Output handler not implemented for unknown jack channel type on %s.%s\n", inst->name, data->port[p].name); + break; + } + + data->port[p].mark = 0; + pthread_mutex_unlock(&data->port[p].lock); + } + } + } + } + + if(config.jack_shutdown){ + fprintf(stderr, "JACK server disconnected\n"); + return 1; + } + return 0; +} + +static int mmjack_start(){ + int rv = 1, feedback_fd[2]; + size_t n, u, p; + instance** inst = NULL; + pthread_mutexattr_t mutex_attr; + mmjack_instance_data* data = NULL; + jack_status_t error; + + //set jack logging functions + jack_set_error_function(mmjack_message_ignore); + if(config.verbosity & 1){ + jack_set_error_function(mmjack_message_print); + } + jack_set_info_function(mmjack_message_ignore); + if(config.verbosity & 2){ + jack_set_info_function(mmjack_message_print); + } + + //prepare mutex attributes because the initializer macro for adaptive mutexes is a GNU extension... + if(pthread_mutexattr_init(&mutex_attr) + || pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ADAPTIVE_NP)){ + fprintf(stderr, "Failed to initialize mutex attributes\n"); + goto bail; + } + + //fetch all instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + goto bail; + } + + for(u = 0; u < n; u++){ + data = (mmjack_instance_data*) inst[u]->impl; + + //connect to the jack server + data->client = jack_client_open(data->client_name ? data->client_name : JACK_DEFAULT_CLIENT_NAME, + JackServerName | JackNoStartServer, + &error, + data->server_name ? data->server_name : JACK_DEFAULT_SERVER_NAME); + + if(!data->client){ + //TODO pretty-print failures + fprintf(stderr, "jack backend failed to connect to server, return status %u\n", error); + goto bail; + } + + //set up the feedback fd + if(socketpair(AF_LOCAL, SOCK_DGRAM, 0, feedback_fd)){ + fprintf(stderr, "Failed to create feedback socket pair\n"); + goto bail; + } + + data->fd = feedback_fd[0]; + if(mm_manage_fd(feedback_fd[1], BACKEND_NAME, 1, inst[u])){ + fprintf(stderr, "jack backend failed to register feedback fd with core\n"); + goto bail; + } + + //connect jack callbacks + jack_set_process_callback(data->client, mmjack_process, inst[u]); + jack_on_shutdown(data->client, mmjack_server_shutdown, inst[u]); + + fprintf(stderr, "jack instance %s assigned client name %s\n", inst[u]->name, jack_get_client_name(data->client)); + + //create and initialize jack ports + for(p = 0; p < data->ports; p++){ + if(pthread_mutex_init(&(data->port[p].lock), &mutex_attr)){ + fprintf(stderr, "Failed to create port mutex\n"); + goto bail; + } + + data->port[p].port = jack_port_register(data->client, + data->port[p].name, + (data->port[p].type == port_cv) ? JACK_DEFAULT_AUDIO_TYPE : JACK_DEFAULT_MIDI_TYPE, + data->port[p].input ? JackPortIsInput : JackPortIsOutput, + 0); + + jack_set_property(data->client, jack_port_uuid(data->port[p].port), JACKEY_SIGNAL_TYPE, "CV", "text/plain"); + + if(!data->port[p].port){ + fprintf(stderr, "Failed to create jack port %s.%s\n", inst[u]->name, data->port[p].name); + return 1; + } + } + + //do the thing + if(jack_activate(data->client)){ + fprintf(stderr, "Failed to activate jack client for instance %s\n", inst[u]->name); + return 1; + } + } + + fprintf(stderr, "jack backend registered %" PRIsize_t " descriptors to core\n", n); + rv = 0; +bail: + pthread_mutexattr_destroy(&mutex_attr); + free(inst); + return rv; +} + +static int mmjack_shutdown(){ + size_t n, u, p; + instance** inst = NULL; + mmjack_instance_data* data = NULL; + + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + for(u = 0; u < n; u++){ + data = (mmjack_instance_data*) inst[u]->impl; + + //deactivate client to stop processing before free'ing channel data + if(data->client){ + jack_deactivate(data->client); + } + + //iterate and close ports + for(p = 0; p < data->ports; p++){ + jack_remove_property(data->client, jack_port_uuid(data->port[p].port), JACKEY_SIGNAL_TYPE); + if(data->port[p].port){ + jack_port_unregister(data->client, data->port[p].port); + } + free(data->port[p].name); + data->port[p].name = NULL; + + free(data->port[p].queue); + data->port[p].queue = NULL; + data->port[p].queue_alloc = data->port[p].queue_len = 0; + + pthread_mutex_destroy(&data->port[p].lock); + } + + //terminate jack connection + if(data->client){ + jack_client_close(data->client); + } + + //clean up instance data + free(data->server_name); + data->server_name = NULL; + free(data->client_name); + data->client_name = NULL; + close(data->fd); + data->fd = -1; + } + + free(inst); + + fprintf(stderr, "jack backend shut down\n"); + return 0; +} diff --git a/backends/jack.h b/backends/jack.h new file mode 100644 index 0000000..dd59cd2 --- /dev/null +++ b/backends/jack.h @@ -0,0 +1,76 @@ +#include "midimonster.h" +#include +#include + +int init(); +static int mmjack_configure(char* option, char* value); +static int mmjack_configure_instance(instance* inst, char* option, char* value); +static instance* mmjack_instance(); +static channel* mmjack_channel(instance* inst, char* spec); +static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v); +static int mmjack_handle(size_t num, managed_fd* fds); +static int mmjack_start(); +static int mmjack_shutdown(); + +#define JACK_DEFAULT_CLIENT_NAME "MIDIMonster" +#define JACK_DEFAULT_SERVER_NAME "default" +#define JACK_MIDIQUEUE_CHUNK 10 + +enum /*mmjack_midi_channel_type*/ { + midi_none = 0, + midi_note = 0x90, + midi_cc = 0xB0, + midi_pressure = 0xA0, + midi_aftertouch = 0xD0, + midi_pitchbend = 0xE0 +}; + +typedef union { + struct { + uint32_t port; + uint8_t pad; + uint8_t sub_type; + uint8_t sub_channel; + uint8_t sub_control; + } fields; + uint64_t label; +} mmjack_channel_ident; + +typedef enum /*_mmjack_port_type*/ { + port_none = 0, + port_midi, + port_osc, + port_cv +} mmjack_port_type; + +typedef struct /*_mmjack_midiqueue_entry*/ { + mmjack_channel_ident ident; + uint16_t raw; +} mmjack_midiqueue; + +typedef struct /*_mmjack_port_data*/ { + char* name; + mmjack_port_type type; + uint8_t input; + jack_port_t* port; + + double max; + double min; + uint8_t mark; + double last; + size_t queue_len; + size_t queue_alloc; + mmjack_midiqueue* queue; + + pthread_mutex_t lock; +} mmjack_port; + +typedef struct /*_jack_instance_data*/ { + char* server_name; + char* client_name; + int fd; + + jack_client_t* client; + size_t ports; + mmjack_port* port; +} mmjack_instance_data; diff --git a/backends/jack.md b/backends/jack.md new file mode 100644 index 0000000..b6ff5a9 --- /dev/null +++ b/backends/jack.md @@ -0,0 +1,84 @@ +### The `jack` backend + +This backend provides read-write access to the JACK Audio Connection Kit low-latency audio transport server for the +transport of control data via either JACK midi ports or control voltage (CV) inputs and outputs. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `debug` | `on` | `off` | Print `info` level notices from the JACK connection | +| `errors` | `on` | `off` | Print `error` level notices from the JACK connection | + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `name` | `Controller` | `MIDIMonster` | Client name for the JACK connection | +| `server` | `jackserver` | `default` | JACK server identifier to connect to | + +Channels (corresponding to JACK ports) need to be configured with their type and, if applicable, value limits. +To configure a port, specify it in the instance configuration using the following syntax: + +``` +port_name = min max +``` + +Port names may be any string except for the instance configuration keywords `name` and `server`. + +The following `type` values are currently supported: + +* `midi`: JACK MIDI port for transmitting MIDI event messages +* `cv`: JACK audio port for transmitting DC offset "control voltage" samples (requires `min`/`max` configuration) + +`direction` may be one of `in` or `out`, as seen from the perspective of the MIDIMonster core, thus +`in` means data is being read from the JACK server and `out` transfers data into the JACK server. + +The following example instance configuration would create a MIDI port sending data into JACK, a control voltage output +sending data between `-1` and `1`, and a control voltage input receiving data with values between `0` and `10`. + +``` +midi_out = midi out +cv_out = cv out min -1 max 1 +cv_in = cv in min 0.0 max 10.0 +``` + +Input CV samples outside the configured range will be clipped. The MIDIMonster will not generate output CV samples +outside of the configured range. + +#### Channel specification + +CV ports are exposed as single MIDIMonster channel and directly map to their normalised values. + +MIDI ports provide subchannels for the various MIDI controls available. Each MIDI port carries +16 MIDI channels (numbered 0 through 15), each of which has 128 note controls (numbered 0 through 127), +corresponding pressure controls for each note, 128 control change (CC) controls (numbered likewise), +one channel wide "aftertouch" control and one channel-wide pitchbend control. + +A MIDI port subchannel is specified using the syntax `channel.`. The shorthand `ch` may be +used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). + +The following values are recognized for `type`: + +* `cc` - Control Changes +* `note` - Note On/Off messages +* `pressure` - Note pressure/aftertouch messages +* `aftertouch` - Channel-wide aftertouch messages +* `pitch` - Channel pitchbend messages + +The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. + +Example mappings: +``` +jack1.cv_in > jack1.midi_out.ch0.note3 +jack1.midi_in.ch0.pitch > jack1.cv_out +``` + +The MIDI subchannel syntax is intentionally kept compatible to the different MIDI backends also supported +by the MIDIMonster + +#### Known bugs / problems + +While JACK has rudimentary capabilities for transporting OSC messages, configuring and parsing such channels +with this backend would take a great amount of dedicated syntax & code. CV ports can provide fine-grained single +control channels as an alternative to MIDI. This feature may be implemented at some point in the future. -- cgit v1.2.3 From 92516e0990145b2b42a9caf7af454d668f56d0cb Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 19:03:24 +0100 Subject: Add libjack-jackd2-dev to build dependencies for CI --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 12de8c3..59274e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ addons: - libasound2-dev - libevdev-dev - libola-dev + - libjack-jackd2-dev - liblua5.3-dev - libssl-dev packages: &core_build_gpp_latest -- cgit v1.2.3 From 7824feeec4477a1ade9bbf22a8a1609513bdaa2b Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 19:05:01 +0100 Subject: Fix spelling to placate spellintian --- backends/jack.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/jack.c b/backends/jack.c index 5a88cf2..f73ada8 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -153,7 +153,7 @@ static int mmjack_process_cv(instance* inst, mmjack_port* port, size_t nframes, if(port->input){ //read updated data into the local buffer - //FIXME maybe we dont want to always use the first sample... + //FIXME maybe we don't want to always use the first sample... if((double) audio_buffer[0] != port->last){ port->last = audio_buffer[0]; port->mark = 1; -- cgit v1.2.3 From be38eb9ddd1b82a87cf26884dd13ccb6dff5eebf Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 21:09:16 +0100 Subject: Try to build the JACK backend on OSX --- .travis.yml | 2 +- backends/Makefile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 59274e4..e1cef00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -180,7 +180,7 @@ install: before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl jack; fi # OpenSSL is not a proper install due to some Apple bull, so provide additional locations via the environment... - export CFLAGS="$CFLAGS -I/usr/local/opt/openssl/include" - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl/lib" diff --git a/backends/Makefile b/backends/Makefile index c5755c9..901ec49 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full -LINUX_BACKENDS = midi.so evdev.so jack.so +LINUX_BACKENDS = midi.so evdev.so WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o -- cgit v1.2.3 From 20eb48ce4ccffe88b22ecd6a93bc9e097e5aa498 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 21:22:09 +0100 Subject: Use default mutex type for OSX --- README.md | 2 +- backends/jack.c | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f23e696..def72d6 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Currently, the MIDIMonster supports the following protocols: | evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | | Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | | MA Lighting Web Remote | Linux, Windows, OSX | GrandMA and dot2 (incl. OnPC) | [`maweb`](backends/maweb.md) | -| JACK/LV2 Control Voltage (CV) | Linux | | [`jack`](backends/jack.md) | +| JACK/LV2 Control Voltage (CV) | Linux, OSX | | [`jack`](backends/jack.md) | with additional flexibility provided by a [Lua scripting environment](backends/lua.md). diff --git a/backends/jack.c b/backends/jack.c index f73ada8..efcd8d5 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -607,7 +607,11 @@ static int mmjack_start(){ //prepare mutex attributes because the initializer macro for adaptive mutexes is a GNU extension... if(pthread_mutexattr_init(&mutex_attr) +#ifndef __APPLE__ || pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ADAPTIVE_NP)){ +#else + } +#endif fprintf(stderr, "Failed to initialize mutex attributes\n"); goto bail; } -- cgit v1.2.3 From 5a79158ec0195cacbc8f4661dff5b26363797447 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 21:26:56 +0100 Subject: Fix Coverity CIDs 350438, 350437 --- backends/jack.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backends/jack.c b/backends/jack.c index efcd8d5..48a6b68 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -550,13 +550,13 @@ static int mmjack_handle(size_t num, managed_fd* fds){ if(num){ for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + data = (mmjack_instance_data*) inst->impl; bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); if(bytes < 0){ fprintf(stderr, "Failed to receive on feedback socket for instance %s\n", inst->name); return 1; } - inst = (instance*) fds[u].impl; - data = (mmjack_instance_data*) inst->impl; for(p = 0; p < data->ports; p++){ if(data->port[p].input && data->port[p].mark){ @@ -672,14 +672,14 @@ static int mmjack_start(){ if(!data->port[p].port){ fprintf(stderr, "Failed to create jack port %s.%s\n", inst[u]->name, data->port[p].name); - return 1; + goto bail; } } //do the thing if(jack_activate(data->client)){ fprintf(stderr, "Failed to activate jack client for instance %s\n", inst[u]->name); - return 1; + goto bail; } } -- cgit v1.2.3 From b1f8732126f23cbd2345faa6ae326cd7b75bab5d Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 21:30:46 +0100 Subject: Fix mutex type for OSX --- backends/jack.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backends/jack.c b/backends/jack.c index 48a6b68..b3aacd4 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -11,6 +11,12 @@ #define BACKEND_NAME "jack" #define JACKEY_SIGNAL_TYPE "http://jackaudio.org/metadata/signal-type" +#ifdef __APPLE__ + #ifndef PTHREAD_MUTEX_ADAPTIVE_NP + #define PTHREAD_MUTEX_ADAPTIVE_NP PTHREAD_MUTEX_DEFAULT + #endif +#endif + //FIXME pitchbend range is somewhat oob static struct /*_mmjack_backend_cfg*/ { @@ -607,11 +613,7 @@ static int mmjack_start(){ //prepare mutex attributes because the initializer macro for adaptive mutexes is a GNU extension... if(pthread_mutexattr_init(&mutex_attr) -#ifndef __APPLE__ || pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_ADAPTIVE_NP)){ -#else - } -#endif fprintf(stderr, "Failed to initialize mutex attributes\n"); goto bail; } -- cgit v1.2.3 From 6c75f07260639fd2bc6d328d5f00c72ab4382fa8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 6 Nov 2019 21:53:19 +0100 Subject: Update README to reflect MIDI on OSX can be done via JACK --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index def72d6..130945a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Currently, the MIDIMonster supports the following protocols: | Protocol | Operating Systems | Notes | Backends | |-------------------------------|-----------------------|-------------------------------|-------------------------------| -| MIDI | Linux, Windows | Linux: via ALSA/JACK | [`midi`](backends/midi.md), [`winmidi`](backends/winmidi.md), [`jack`](backends/jack.md) | +| MIDI | Linux, Windows, OSX | Linux: via ALSA/JACK, OSX: via JACK | [`midi`](backends/midi.md), [`winmidi`](backends/winmidi.md), [`jack`](backends/jack.md) | | 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) | -- cgit v1.2.3 From 350f0d2d2eaff5f0d57b09857102e2df1e96d733 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 7 Nov 2019 18:44:19 +0100 Subject: Makefile install target and packaging instructions (Fixes #28) --- Makefile | 22 +++++++++++++++++----- README.md | 23 +++++++++++++++++++++++ backends/lua.md | 5 +---- backends/maweb.c | 2 +- backends/winmidi.c | 2 +- config.c | 38 +++++++++++++++++++++++++++++++++++--- configs/flying-faders.cfg | 2 +- configs/lua.cfg | 2 +- midimonster.h | 13 ++++++++++++- plugin.c | 12 ++++++++++-- 10 files changed, 102 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index 2d88c49..5a83a2d 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,7 @@ -.PHONY: all clean run sanitize backends windows full backends-full +.PHONY: all clean run sanitize backends windows full backends-full install OBJS = config.o backend.o plugin.o -PLUGINDIR = "\"./backends/\"" -PLUGINDIR_W32 = "\"backends\\\\\"" +PREFIX ?= /usr SYSTEM := $(shell uname -s) CFLAGS ?= -g -Wall -Wpedantic @@ -11,7 +10,6 @@ CFLAGS += -fvisibility=hidden #CFLAGS += -DDEBUG midimonster: LDLIBS = -ldl -midimonster: CFLAGS += -DPLUGINS=$(PLUGINDIR) # Work around strange linker passing convention differences in Linux and OSX ifeq ($(SYSTEM),Linux) @@ -21,6 +19,14 @@ ifeq ($(SYSTEM),Darwin) midimonster: LDFLAGS += -Wl,-export_dynamic endif +# Allow overriding the locations for backend plugins and default configuration +ifdef DEFAULT_CFG +midimonster: CFLAGS += -DDEFAULT_CFG=\"$(DEFAULT_CFG)\" +endif +ifdef PLUGINS +midimonster: CFLAGS += -DPLUGINS=\"$(PLUGINS)\" +endif + all: midimonster backends full: midimonster backends-full @@ -39,7 +45,7 @@ midimonster: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ midimonster.exe: export CC = x86_64-w64-mingw32-gcc -midimonster.exe: CFLAGS += -DPLUGINS=$(PLUGINDIR_W32) -Wno-format +midimonster.exe: CFLAGS += -Wno-format midimonster.exe: LDLIBS = -lws2_32 midimonster.exe: LDFLAGS += -Wl,--out-implib,libmmapi.a midimonster.exe: midimonster.c portability.h $(OBJS) @@ -55,6 +61,12 @@ clean: run: valgrind --leak-check=full --show-leak-kinds=all ./midimonster +install: + install -d "$(DESTDIR)$(PREFIX)/bin" + install -d "$(DESTDIR)$(PREFIX)/lib/midimonster" + install -m 0755 midimonster "$(DESTDIR)$(PREFIX)/bin" + install -m 0755 backends/*.so "$(DESTDIR)$(PREFIX)/lib/midimonster" + sanitize: export CC = clang sanitize: export CFLAGS += -g -Wall -Wpedantic -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer sanitize: midimonster backends diff --git a/README.md b/README.md index 130945a..c31f16c 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,16 @@ be used to build a subset of the backends as well as the core. For Linux and OSX, just running `make` in the source directory should do the trick. +The build process accepts the following parameters, either from the environment or +as arguments to the `make` invocation: + +| Target | Parameter | Default value | Description | +|---------------|-----------------------|-------------------------------|-------------------------------| +| build targets | `DEFAULT_CFG` | `monster.cfg` | Default configuration file | +| build targets | `PLUGINS` | Linux/OSX: `./backends/`, Windows: `backends\` | Backend plugin library path | +| `install` | `DESTDIR` | empty | Destination directory for packaging builds | +| `install` | `PREFIX` | `/usr` | Install prefix for binaries | + Some backends have been marked as optional as they require rather large additional software to be installed, for example the `ola` backend. To create a build including these, run `make full`. @@ -163,6 +173,19 @@ To build for Windows, you still need to compile on a Linux machine. Install the crosscompiler package listed above and run `make windows`. This will build `midimonster.exe` as well as a set of backends as DLL files. +For system-wide install or packaging builds, the following steps are recommended: + +``` +export PREFIX=/usr +export PLUGINS=$PREFIX/lib/midimonster +export DEFAULT_CFG=/etc/midimonster.cfg +make +make install +``` + +Depending on your configuration of `DESTDIR`, the `make install` step may require root privileges to +install the binaries to the appropriate destinations. + ## Development The architecture is split into the `midimonster` core, handling mapping diff --git a/backends/lua.md b/backends/lua.md index 6ad5c2a..f38e189 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -43,7 +43,7 @@ The `lua` backend does not take any global configuration. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `script` | `script.lua` | none | Lua source file | +| `script` | `script.lua` | none | Lua source file (relative to configuration file)| A single instance may have multiple `source` options specified, which will all be read cumulatively. @@ -64,6 +64,3 @@ Using these names as arguments to the output and value interface functions works Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. - -The path to the Lua source files is relative to the current working directory. This may lead -to problems when copying configuration between installations. diff --git a/backends/maweb.c b/backends/maweb.c index c98d04b..57d04ae 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -470,7 +470,7 @@ static int maweb_request_playbacks(instance* inst){ offsets[0] = offsets[1] = offsets[2] = 1; page_index = data->channel[channel].page; //poll logic differs between the consoles because reasons - //dont quote me on this section + //don't quote me on this section if(data->peer_type == peer_dot2){ //blocks 0, 100 & 200 have 21 execs and need to be queried from fader view view = (data->channel[channel].index >= 300) ? 3 : 2; diff --git a/backends/winmidi.c b/backends/winmidi.c index de7d867..d6bc0bc 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -84,7 +84,7 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) } if(!strcmp(option, "write")){ if(data->write){ - fprintf(stderr, "winmidi instance %s already connected to an otput device\n", inst->name); + fprintf(stderr, "winmidi instance %s already connected to an output device\n", inst->name); return 1; } data->write = strdup(value); diff --git a/config.c b/config.c index f4d928e..ddee720 100644 --- a/config.c +++ b/config.c @@ -1,5 +1,7 @@ #include #include +#include +#include #include "midimonster.h" #include "config.h" #include "backend.h" @@ -310,16 +312,45 @@ done: return rv; } -int config_read(char* cfg_file){ +int config_read(char* cfg_filepath){ int rv = 1; size_t line_alloc = 0; ssize_t status; map_type mapping_type = map_rtl; char* line_raw = NULL, *line, *separator; - FILE* source = fopen(cfg_file, "r"); + + //create heap copy of file name because original might be in readonly memory + char* source_dir = strdup(cfg_filepath), *source_file = NULL; + #ifdef _WIN32 + char path_separator = '\\'; + #else + char path_separator = '/'; + #endif + + if(!source_dir){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + //change working directory to the one containing the configuration file so relative paths work as expected + source_file = strrchr(source_dir, path_separator); + if(source_file){ + *source_file = 0; + source_file++; + if(chdir(source_dir)){ + fprintf(stderr, "Failed to change to configuration file directory %s: %s\n", source_dir, strerror(errno)); + goto bail; + } + } + else{ + source_file = source_dir; + } + + FILE* source = fopen(source_file, "r"); + if(!source){ fprintf(stderr, "Failed to open configuration file for reading\n"); - return 1; + goto bail; } for(status = getline(&line_raw, &line_alloc, source); status >= 0; status = getline(&line_raw, &line_alloc, source)){ @@ -459,6 +490,7 @@ int config_read(char* cfg_file){ rv = 0; bail: + free(source_dir); fclose(source); free(line_raw); return rv; diff --git a/configs/flying-faders.cfg b/configs/flying-faders.cfg index 4197581..d331f38 100644 --- a/configs/flying-faders.cfg +++ b/configs/flying-faders.cfg @@ -13,7 +13,7 @@ dest = learn@9000 /1/xy = ff 0.0 1.0 0.0 1.0 [lua generator] -script = configs/flying-faders.lua +script = flying-faders.lua [map] diff --git a/configs/lua.cfg b/configs/lua.cfg index af17496..098c0f1 100644 --- a/configs/lua.cfg +++ b/configs/lua.cfg @@ -13,7 +13,7 @@ axis.ABS_X = 34300 0 65535 255 4095 axis.ABS_Y = 34300 0 65535 255 4095 [lua lua] -script = configs/demo.lua +script = demo.lua [artnet art] universe = 0 diff --git a/midimonster.h b/midimonster.h index 491cc11..b05326c 100644 --- a/midimonster.h +++ b/midimonster.h @@ -33,7 +33,18 @@ #include "portability.h" /* Default configuration file name to read when no other is specified */ -#define DEFAULT_CFG "monster.cfg" +#ifndef DEFAULT_CFG + #define DEFAULT_CFG "monster.cfg" +#endif + +/* Default backend plugin location */ +#ifndef PLUGINS + #ifndef _WIN32 + #define PLUGINS "./backends/" + #else + #define PLUGINS "backends\\" + #endif +#endif /* Forward declare some of the structs so we can use them in each other */ struct _channel_value; diff --git a/plugin.c b/plugin.c index dd99041..a14baff 100644 --- a/plugin.c +++ b/plugin.c @@ -24,13 +24,21 @@ static int plugin_attach(char* path, char* file){ plugin_init init = NULL; void* handle = NULL; char* lib = NULL; + #ifdef _WIN32 + char* path_separator = "\\"; + #else + char* path_separator = "/"; + #endif - lib = calloc(strlen(path) + strlen(file) + 1, sizeof(char)); + lib = calloc(strlen(path) + strlen(file) + 2, sizeof(char)); if(!lib){ fprintf(stderr, "Failed to allocate memory\n"); return 1; } - snprintf(lib, strlen(path) + strlen(file) + 1, "%s%s", path, file); + snprintf(lib, strlen(path) + strlen(file) + 2, "%s%s%s", + path, + (path[strlen(path)] == path_separator[0]) ? "" : path_separator, + file); handle = dlopen(lib, RTLD_NOW); if(!handle){ -- cgit v1.2.3 From 988bcbe79115e55929390bcac5493abb4ca20011 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 7 Nov 2019 19:00:37 +0100 Subject: Only fclose configuration file if it has been opened previously (Coverity CID 350443) --- config.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config.c b/config.c index ddee720..8e7e581 100644 --- a/config.c +++ b/config.c @@ -491,7 +491,9 @@ int config_read(char* cfg_filepath){ rv = 0; bail: free(source_dir); - fclose(source); + if(source){ + fclose(source); + } free(line_raw); return rv; } -- cgit v1.2.3 From ddbc61cf653120b513e4e76d893909ba4a77b97f Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 9 Nov 2019 14:00:29 +0100 Subject: Fix configurations --- configs/launchctl-sacn.cfg | 7 +++---- configs/maweb-flying-faders.cfg | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/configs/launchctl-sacn.cfg b/configs/launchctl-sacn.cfg index 781ca40..0f4a19b 100644 --- a/configs/launchctl-sacn.cfg +++ b/configs/launchctl-sacn.cfg @@ -6,15 +6,14 @@ [backend midi] name = MIDIMonster -[backend artnet] +[backend sacn] bind = 0.0.0.0 [midi lc] read = Launch Control -[artnet out] -universe = 0 -destination = 255.255.255.255 +[sacn out] +universe = 1 [map] diff --git a/configs/maweb-flying-faders.cfg b/configs/maweb-flying-faders.cfg index 03b0939..806c4d4 100644 --- a/configs/maweb-flying-faders.cfg +++ b/configs/maweb-flying-faders.cfg @@ -8,7 +8,7 @@ user = midimonster password = midimonster [lua generator] -script = configs/flying-faders.lua +script = flying-faders.lua [map] ; Fader 1 to 6 (Core Wing) -- cgit v1.2.3 From 216bde68d3cca8f5f4dca2d97ae6c273037309cd Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 10 Nov 2019 14:12:37 +0100 Subject: Add clean step to packaging instructions --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c31f16c..c8da0c1 100644 --- a/README.md +++ b/README.md @@ -169,9 +169,12 @@ as arguments to the `make` invocation: Some backends have been marked as optional as they require rather large additional software to be installed, for example the `ola` backend. To create a build including these, run `make full`. -To build for Windows, you still need to compile on a Linux machine. -Install the crosscompiler package listed above and run `make windows`. -This will build `midimonster.exe` as well as a set of backends as DLL files. +Backends may also be built selectively by runnning `make ` in the `backends/` directory, +for example + +``` +make jack.so +``` For system-wide install or packaging builds, the following steps are recommended: @@ -179,13 +182,18 @@ For system-wide install or packaging builds, the following steps are recommended export PREFIX=/usr export PLUGINS=$PREFIX/lib/midimonster export DEFAULT_CFG=/etc/midimonster.cfg -make +make clean +make full make install ``` Depending on your configuration of `DESTDIR`, the `make install` step may require root privileges to install the binaries to the appropriate destinations. +To build for Windows, you still need to compile on a Linux machine. Install the `mingw-w64` crosscompiler package +and run `make windows` in the project directory. This will build `midimonster.exe` as well as a set of backends +as DLL files. + ## Development The architecture is split into the `midimonster` core, handling mapping -- cgit v1.2.3 From aed52ce3e67f35c69ef1295f770a012258167496 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 10 Nov 2019 14:24:51 +0100 Subject: Actually use plugin install path in Makefile --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5a83a2d..40570c8 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ OBJS = config.o backend.o plugin.o PREFIX ?= /usr +PLUGIN_INSTALL = "$(PREFIX)/lib/midimonster" SYSTEM := $(shell uname -s) CFLAGS ?= -g -Wall -Wpedantic @@ -25,6 +26,7 @@ midimonster: CFLAGS += -DDEFAULT_CFG=\"$(DEFAULT_CFG)\" endif ifdef PLUGINS midimonster: CFLAGS += -DPLUGINS=\"$(PLUGINS)\" +PLUGIN_INSTALL = $(PLUGINS) endif all: midimonster backends @@ -63,7 +65,7 @@ run: install: install -d "$(DESTDIR)$(PREFIX)/bin" - install -d "$(DESTDIR)$(PREFIX)/lib/midimonster" + install -d "$(DESTDIR)$(PLUGIN_INSTALL)" install -m 0755 midimonster "$(DESTDIR)$(PREFIX)/bin" install -m 0755 backends/*.so "$(DESTDIR)$(PREFIX)/lib/midimonster" -- cgit v1.2.3 From ccde53920cce56c2873c6cf35460fd74518dc79b Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 10 Nov 2019 14:27:07 +0100 Subject: Fix spelling --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c8da0c1..9af09b3 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ as arguments to the `make` invocation: Some backends have been marked as optional as they require rather large additional software to be installed, for example the `ola` backend. To create a build including these, run `make full`. -Backends may also be built selectively by runnning `make ` in the `backends/` directory, +Backends may also be built selectively by running `make ` in the `backends/` directory, for example ``` -- cgit v1.2.3 From a7ca0def9e36912f582bd9f21dbc68808b3fd037 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 13 Nov 2019 22:23:10 +0100 Subject: Extend build instructions (Fixes #32) --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9af09b3..dd728f0 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,8 @@ for example make jack.so ``` +#### Building for packaging or installation + For system-wide install or packaging builds, the following steps are recommended: ``` @@ -190,9 +192,22 @@ make install Depending on your configuration of `DESTDIR`, the `make install` step may require root privileges to install the binaries to the appropriate destinations. -To build for Windows, you still need to compile on a Linux machine. Install the `mingw-w64` crosscompiler package -and run `make windows` in the project directory. This will build `midimonster.exe` as well as a set of backends -as DLL files. +#### Building for Windows + +To build for Windows, you still need to compile on a Linux machine (virtual machines work well for this). + +In a fresh Debian installation, you will need to install the following packages (using `apt-get install` as root): + * `build-essential` + * `pkg-config` + * `git` + * `mingw-w64` + +Clone the repository and run `make windows` in the project directory. +This will build `midimonster.exe` as well as a set of backends as DLL files, which you can then copy +to the Windows machine. + +Note that some backends have limitations when building on Windows (refer to the backend documentation +for detailed information). ## Development -- cgit v1.2.3 From 95bf6f5f98e493ff9d4f630d8a88343b5349def0 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 13 Nov 2019 23:15:54 +0100 Subject: Fix list formatting --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dd728f0..6b08018 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,7 @@ install the binaries to the appropriate destinations. To build for Windows, you still need to compile on a Linux machine (virtual machines work well for this). In a fresh Debian installation, you will need to install the following packages (using `apt-get install` as root): + * `build-essential` * `pkg-config` * `git` -- cgit v1.2.3 From cb17ab98ebffb5b736068452da5a46016caba424 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 13 Nov 2019 23:16:46 +0100 Subject: Fix package listing... --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6b08018..83cd06b 100644 --- a/README.md +++ b/README.md @@ -198,10 +198,10 @@ To build for Windows, you still need to compile on a Linux machine (virtual mach In a fresh Debian installation, you will need to install the following packages (using `apt-get install` as root): - * `build-essential` - * `pkg-config` - * `git` - * `mingw-w64` +* `build-essential` +* `pkg-config` +* `git` +* `mingw-w64` Clone the repository and run `make windows` in the project directory. This will build `midimonster.exe` as well as a set of backends as DLL files, which you can then copy -- cgit v1.2.3 From b4a9721ff7d93dd899eb8e6c64510afaf95cbbca Mon Sep 17 00:00:00 2001 From: Leon Scheid Date: Thu, 21 Nov 2019 18:41:02 +0100 Subject: Chane NoteOff behavior --- backends/midi.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index 536457d..d609ad9 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -259,14 +259,20 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.label = 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; event_type = "note"; break; + case SND_SEQ_EVENT_NOTEOFF: + ident.fields.type = note; + ident.fields.channel = ev->data.note.channel; + ident.fields.control = ev->data.note.note; + val.normalised = (double)0; + event_type = "note"; + break; + case SND_SEQ_EVENT_NOTE: case SND_SEQ_EVENT_KEYPRESS: ident.fields.type = pressure; ident.fields.channel = ev->data.note.channel; -- cgit v1.2.3 From 243d176936152bc2691a5594f76583948af70618 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 22 Nov 2019 14:45:33 +0100 Subject: Exclude buttons from feedback filtering for maweb backend --- backends/maweb.c | 4 +++- backends/winmidi.c | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 57d04ae..453dfa4 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -837,7 +837,9 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ //i/o value space separation chan->in = v[n].normalised; - chan->input_blocked = 1; + if(chan->type == exec_fader){ + chan->input_blocked = 1; + } switch(chan->type){ case exec_fader: diff --git a/backends/winmidi.c b/backends/winmidi.c index d6bc0bc..ffca3b4 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -277,17 +277,19 @@ static int winmidi_handle(size_t num, managed_fd* fds){ //pretty-print channel-wide events if(backend_config.event[u].channel.fields.type == pitchbend || backend_config.event[u].channel.fields.type == aftertouch){ - fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s\n", + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s, value %f\n", backend_config.event[u].inst->name, backend_config.event[u].channel.fields.channel, - winmidi_type_name(backend_config.event[u].channel.fields.type)); + winmidi_type_name(backend_config.event[u].channel.fields.type), + backend_config.event[u].value); } else{ - fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d\n", + fprintf(stderr, "Incoming MIDI data on channel %s.ch%d.%s%d, value %f\n", backend_config.event[u].inst->name, backend_config.event[u].channel.fields.channel, winmidi_type_name(backend_config.event[u].channel.fields.type), - backend_config.event[u].channel.fields.control); + backend_config.event[u].channel.fields.control, + backend_config.event[u].value); } } chan = mm_channel(backend_config.event[u].inst, backend_config.event[u].channel.label, 0); @@ -434,7 +436,7 @@ static int winmidi_match_input(char* prefix){ printf("\tID %d: %s\n", n, input_caps.szPname); } else if(!strncmp(input_caps.szPname, prefix, strlen(prefix))){ - fprintf(stderr, "winmidi selected input device %s for name %s\n", input_caps.szPname, prefix); + fprintf(stderr, "winmidi selected input device %s (ID %" PRIsize_t ") for name %s\n", input_caps.szPname, n, prefix); return n; } } @@ -467,7 +469,7 @@ static int winmidi_match_output(char* prefix){ printf("\tID %d: %s\n", n, output_caps.szPname); } else if(!strncmp(output_caps.szPname, prefix, strlen(prefix))){ - fprintf(stderr, "winmidi selected output device %s for name %s\n", output_caps.szPname, prefix); + fprintf(stderr, "winmidi selected output device %s (ID %" PRIsize_t " for name %s\n", output_caps.szPname, n, prefix); return n; } } -- cgit v1.2.3 From b60c1adbf3c66c42997020539f8bb8a0eb6250c9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 22 Nov 2019 14:56:36 +0100 Subject: Update button control state on input not output --- backends/maweb.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index 453dfa4..c495512 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -835,10 +835,10 @@ static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v){ } chan->out = v[n].normalised; - //i/o value space separation - chan->in = v[n].normalised; + //i/o value space separation & feedback filtering for faders if(chan->type == exec_fader){ chan->input_blocked = 1; + chan->in = v[n].normalised; } switch(chan->type){ -- cgit v1.2.3 From d29a499113d389a8bee9bf21bf5969d41c9cf307 Mon Sep 17 00:00:00 2001 From: Leon Scheid Date: Thu, 21 Nov 2019 18:41:02 +0100 Subject: Chane NoteOff behavior --- backends/midi.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index 536457d..d609ad9 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -259,14 +259,20 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.label = 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; event_type = "note"; break; + case SND_SEQ_EVENT_NOTEOFF: + ident.fields.type = note; + ident.fields.channel = ev->data.note.channel; + ident.fields.control = ev->data.note.note; + val.normalised = (double)0; + event_type = "note"; + break; + case SND_SEQ_EVENT_NOTE: case SND_SEQ_EVENT_KEYPRESS: ident.fields.type = pressure; ident.fields.channel = ev->data.note.channel; -- cgit v1.2.3 From 1aa8b88a1df5539fa7ad4004cfcbf3b8438486b4 Mon Sep 17 00:00:00 2001 From: Leon Scheid Date: Fri, 22 Nov 2019 15:40:11 +0100 Subject: Remove unnecessary case --- backends/midi.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index d609ad9..cde278b 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -259,20 +259,17 @@ static int midi_handle(size_t num, managed_fd* fds){ ident.label = 0; switch(ev->type){ case SND_SEQ_EVENT_NOTEON: - 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; - event_type = "note"; - break; 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)0; + 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_NOTE: case SND_SEQ_EVENT_KEYPRESS: ident.fields.type = pressure; ident.fields.channel = ev->data.note.channel; -- cgit v1.2.3 From dadadbe4aa8028197cfaaedc37bceb920923b803 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 22 Nov 2019 20:21:25 +0100 Subject: Fix OSX build by yet again hardcoding things that should have been unnecessary --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e1cef00..bdaf63a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -182,8 +182,10 @@ before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl jack; fi # OpenSSL is not a proper install due to some Apple bull, so provide additional locations via the environment... - - export CFLAGS="$CFLAGS -I/usr/local/opt/openssl/include" - - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl/lib" +# Additionally, newer versions of this "recipe" seem to use the name 'openssl@1.1' instead of plain 'openssl' and there seems to be +# no way to programmatically get the link and include paths. Genius! Hardcoding the new version for the time being... + - export CFLAGS="$CFLAGS -I/usr/local/opt/openssl@1.1/include" + - export LDFLAGS="$LDFLAGS -L/usr/local/opt/openssl@1.1/lib" - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then PATH=/usr/local/opt/ccache/libexec:$PATH; fi # Use ccache on Mac too #Coverity doesn't work with g++ 5 or 6, so only upgrade to g++ 4.9 for that - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) -a "$CC" = "gcc" ]; then export CC="ccache gcc-8"; export CXX="ccache g++-8"; fi -- cgit v1.2.3 From 5b840d986ae723656aad4163e12f7d24a88e1da3 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 1 Dec 2019 12:56:22 +0100 Subject: Add configuration files and examples to install target --- Makefile | 11 ++++++++--- monster.cfg | 40 ++++------------------------------------ 2 files changed, 12 insertions(+), 39 deletions(-) diff --git a/Makefile b/Makefile index 40570c8..caad174 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ OBJS = config.o backend.o plugin.o PREFIX ?= /usr -PLUGIN_INSTALL = "$(PREFIX)/lib/midimonster" +PLUGIN_INSTALL = $(PREFIX)/lib/midimonster SYSTEM := $(shell uname -s) CFLAGS ?= -g -Wall -Wpedantic @@ -65,9 +65,14 @@ run: install: install -d "$(DESTDIR)$(PREFIX)/bin" - install -d "$(DESTDIR)$(PLUGIN_INSTALL)" install -m 0755 midimonster "$(DESTDIR)$(PREFIX)/bin" - install -m 0755 backends/*.so "$(DESTDIR)$(PREFIX)/lib/midimonster" + install -d "$(DESTDIR)$(PLUGIN_INSTALL)" + install -m 0755 backends/*.so "$(DESTDIR)$(PLUGIN_INSTALL)" + install -d "$(DESTDIR)$(PREFIX)/share/midimonster" + install -m 0644 configs/* "$(DESTDIR)$(PREFIX)/share/midimonster" +ifdef DEFAULT_CFG + install -m 0644 monster.cfg "$(DESTDIR)$(DEFAULT_CFG)" +endif sanitize: export CC = clang sanitize: export CFLAGS += -g -Wall -Wpedantic -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer diff --git a/monster.cfg b/monster.cfg index e6258a7..8e415a3 100644 --- a/monster.cfg +++ b/monster.cfg @@ -1,39 +1,7 @@ -[backend artnet] -bind = 0.0.0.0 - +; This is a useless default configuration +; Replace it with a proper one from the configs/ directory or write your own :) [loopback loop] -[artnet art] -universe = 0 -dest = 255.255.255.255 - -[backend midi] -detect = on - -[backend evdev] -;detect = on - -[midi bcf] -read = BCF -write = BCF - -[evdev mouse] -input = TPPS -relaxis.REL_X = 255 -relaxis.REL_Y = -255 - -[maweb ma] -;host = 10.23.23.248 -host = 127.0.0.1 4040 -user = web -password = web - [map] -bcf.channel{0..7}.pitch > bcf.channel{0..7}.pitch -bcf.channel{0..7}.pitch > art.{1..8} - -bcf.channel{0..7}.pitch > ma.page1.fader{1..8} -bcf.channel0.note{16..23} > ma.page1.upper{1..8} -bcf.channel0.note{24..31} > ma.page1.lower{1..8} -mouse.EV_REL.REL_Y > ma.page1.fader1 -mouse.EV_KEY.BTN_LEFT > ma.page2.button102 +loop.a > loop.b +loop.b < loop.c -- cgit v1.2.3 From 30feadde5b18f49fd853f8ce61d85168db912bb6 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 1 Dec 2019 13:30:43 +0100 Subject: Fix minor error in maweb documentation --- backends/maweb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/maweb.md b/backends/maweb.md index 5b996a9..45dc778 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -53,7 +53,7 @@ Currently, three types of MA controls can be assigned, with each having some sub * For the dot2, executors are also arranged in pages, but the controls are non-obviously numbered. * For the faders, they are numerically right-to-left from the Core Fader section (Faders 6 to 1) over the F-Wing 1 (Faders 13 to 6) to F-Wing 2 (Faders 21 to 14). - * Above the fader sections are two rows of 21 `button` executors, numbered 122 through 101 (upper row) and 222 through 201 (lower row), + * Above the fader sections are two rows of 21 `button` executors, numbered 122 through 101 (lower row) and 222 through 201 (upper row), in the same order as the faders are. * Fader executors have two buttons below them (`upper` and `lower`). * The button executor section consists of six rows of 16 buttons, divided into two button wings. Buttons on the wings -- cgit v1.2.3 From a0831a2b970404eaa9b80ff97ab46ee759131414 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 3 Dec 2019 22:37:45 +0100 Subject: Add error checking for shell callouts during build --- backends/Makefile | 8 ++++---- midimonster.c | 2 +- midimonster.h | 5 +++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/backends/Makefile b/backends/Makefile index 901ec49..4e37ca4 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -43,12 +43,12 @@ winmidi.dll: LDLIBS += -lwinmm -lws2_32 jack.so: LDLIBS = -ljack -lpthread midi.so: LDLIBS = -lasound -evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev) -evdev.so: LDLIBS = $(shell pkg-config --libs libevdev) +evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev || echo "-DBUILD_ERROR=\"Missing pkg-config data for libevdev\"") +evdev.so: LDLIBS = $(shell pkg-config --libs libevdev || echo "-DBUILD_ERROR=\"Missing pkg-config data for libevdev\"") ola.so: LDLIBS = -lola ola.so: CPPFLAGS += -Wno-write-strings -lua.so: CFLAGS += $(shell pkg-config --cflags lua5.3) -lua.so: LDLIBS += $(shell pkg-config --libs lua5.3) +lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") +lua.so: LDLIBS += $(shell pkg-config --libs lua53 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) diff --git a/midimonster.c b/midimonster.c index 25cf4a0..eb64974 100644 --- a/midimonster.c +++ b/midimonster.c @@ -224,7 +224,7 @@ static void event_free(){ } static int usage(char* fn){ - fprintf(stderr, "MIDIMonster v0.1\n"); + fprintf(stderr, "MIDIMonster v0.2\n"); fprintf(stderr, "Usage:\n"); fprintf(stderr, "\t%s \n", fn); return EXIT_FAILURE; diff --git a/midimonster.h b/midimonster.h index b05326c..3922b03 100644 --- a/midimonster.h +++ b/midimonster.h @@ -29,6 +29,11 @@ #define DBG(message) #endif +/* Stop compilation if the build system reports an error */ +#ifdef BUILD_ERROR + #error The build system reported an error, compilation stopped. Refer to the invocation for this compilation unit for more information. +#endif + /* Pull in additional defines for non-linux platforms */ #include "portability.h" -- cgit v1.2.3 From 4a86ad5ac36c54de1bff61d00b80734da226e37e Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 3 Dec 2019 23:04:45 +0100 Subject: Fix the build on OSX once again --- backends/Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backends/Makefile b/backends/Makefile index 4e37ca4..75ce74b 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -47,8 +47,10 @@ evdev.so: CFLAGS += $(shell pkg-config --cflags libevdev || echo "-DBUILD_ERROR= evdev.so: LDLIBS = $(shell pkg-config --libs libevdev || echo "-DBUILD_ERROR=\"Missing pkg-config data for libevdev\"") ola.so: LDLIBS = -lola ola.so: CPPFLAGS += -Wno-write-strings -lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") -lua.so: LDLIBS += $(shell pkg-config --libs lua53 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") +# The pkg-config name for liblua5.3 is subject to discussion. I prefer 'lua5.3' (which works on Debian and OSX), +# but Arch requires 'lua53' which works on Debian, too, but breaks on OSX. +lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") +lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) -- cgit v1.2.3 From dcdf802e10b8199bf04139b0e63912bda2b681ce Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 3 Dec 2019 23:06:51 +0100 Subject: Fix the flags for the pkg-build invocation on OSX --- backends/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/Makefile b/backends/Makefile index 75ce74b..feefd7b 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -50,7 +50,7 @@ ola.so: CPPFLAGS += -Wno-write-strings # The pkg-config name for liblua5.3 is subject to discussion. I prefer 'lua5.3' (which works on Debian and OSX), # but Arch requires 'lua53' which works on Debian, too, but breaks on OSX. lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") -lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") +lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) -- cgit v1.2.3 From bd9ab579eba35db70ad1ce17d556aeda5866156d Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Dec 2019 01:10:12 +0100 Subject: Hide lua_interval if timerfd callback mechanism is used --- backends/lua.c | 2 ++ backends/lua.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/backends/lua.c b/backends/lua.c index 1cd965e..3555e72 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -52,6 +52,7 @@ int init(){ return 0; } +#ifndef MMBACKEND_LUA_TIMERFD static uint32_t lua_interval(){ size_t n = 0; uint64_t next_timer = 1000; @@ -66,6 +67,7 @@ static uint32_t lua_interval(){ } return 1000; } +#endif static int lua_update_timerfd(){ uint64_t interval = 0, gcd, residual; diff --git a/backends/lua.h b/backends/lua.h index 7aad891..f2583a8 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -18,7 +18,9 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v); static int lua_handle(size_t num, managed_fd* fds); static int lua_start(); static int lua_shutdown(); +#ifndef MMBACKEND_LUA_TIMERFD static uint32_t lua_interval(); +#endif typedef struct /*_lua_instance_data*/ { size_t channels; -- cgit v1.2.3 From 1107a91861189d28d771d02d721d61b403aac38a Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Dec 2019 01:21:14 +0100 Subject: Explicitly mark the backend init symbol visible --- backends/artnet.c | 2 +- backends/artnet.h | 2 +- backends/evdev.c | 2 +- backends/evdev.h | 2 +- backends/jack.c | 4 +++- backends/jack.h | 2 +- backends/loopback.c | 2 +- backends/loopback.h | 2 +- backends/lua.c | 2 +- backends/lua.h | 2 +- backends/maweb.c | 2 +- backends/maweb.h | 2 +- backends/midi.c | 2 +- backends/midi.h | 2 +- backends/ola.cpp | 2 +- backends/ola.h | 2 +- backends/osc.c | 2 +- backends/osc.h | 2 +- backends/sacn.c | 2 +- backends/sacn.h | 2 +- backends/winmidi.c | 2 +- backends/winmidi.h | 2 +- midimonster.h | 10 ++++++++++ 23 files changed, 34 insertions(+), 22 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index e01ac94..8a62a43 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -41,7 +41,7 @@ static int artnet_listener(char* host, char* port){ return 0; } -int init(){ +MM_PLUGIN_API int init(){ backend artnet = { .name = BACKEND_NAME, .conf = artnet_configure, diff --git a/backends/artnet.h b/backends/artnet.h index f5aa745..cce11d1 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -3,7 +3,7 @@ #endif #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int artnet_configure(char* option, char* value); static int artnet_configure_instance(instance* instance, char* option, char* value); static instance* artnet_instance(); diff --git a/backends/evdev.c b/backends/evdev.c index b19cda8..dd2231b 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -24,7 +24,7 @@ static struct { .detect = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend evdev = { .name = BACKEND_NAME, .conf = evdev_configure, diff --git a/backends/evdev.h b/backends/evdev.h index 48bd0ab..30ce892 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -8,7 +8,7 @@ * disabled by building with -DEVDEV_NO_UINPUT */ -int init(); +MM_PLUGIN_API int init(); static int evdev_configure(char* option, char* value); static int evdev_configure_instance(instance* instance, char* option, char* value); static instance* evdev_instance(); diff --git a/backends/jack.c b/backends/jack.c index b3aacd4..e8a63bc 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -4,6 +4,8 @@ #include #include +#define DEBUG + #include "jack.h" #include #include @@ -27,7 +29,7 @@ static struct /*_mmjack_backend_cfg*/ { .jack_shutdown = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend mmjack = { .name = BACKEND_NAME, .conf = mmjack_configure, diff --git a/backends/jack.h b/backends/jack.h index dd59cd2..5598042 100644 --- a/backends/jack.h +++ b/backends/jack.h @@ -2,7 +2,7 @@ #include #include -int init(); +MM_PLUGIN_API int init(); static int mmjack_configure(char* option, char* value); static int mmjack_configure_instance(instance* inst, char* option, char* value); static instance* mmjack_instance(); diff --git a/backends/loopback.c b/backends/loopback.c index 083a312..0a45bde 100644 --- a/backends/loopback.c +++ b/backends/loopback.c @@ -3,7 +3,7 @@ #define BACKEND_NAME "loopback" -int init(){ +MM_PLUGIN_API int init(){ backend loopback = { .name = BACKEND_NAME, .conf = loopback_configure, diff --git a/backends/loopback.h b/backends/loopback.h index c73ca20..a08417b 100644 --- a/backends/loopback.h +++ b/backends/loopback.h @@ -1,6 +1,6 @@ #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int loopback_configure(char* option, char* value); static int loopback_configure_instance(instance* inst, char* option, char* value); static instance* loopback_instance(); diff --git a/backends/lua.c b/backends/lua.c index 3555e72..0b47b2c 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -19,7 +19,7 @@ static int timer_fd = -1; static uint64_t last_timestamp; #endif -int init(){ +MM_PLUGIN_API int init(){ backend lua = { #ifndef MMBACKEND_LUA_TIMERFD .interval = lua_interval, diff --git a/backends/lua.h b/backends/lua.h index f2583a8..e187a8e 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -9,7 +9,7 @@ #define MMBACKEND_LUA_TIMERFD #endif -int init(); +MM_PLUGIN_API int init(); static int lua_configure(char* option, char* value); static int lua_configure_instance(instance* inst, char* option, char* value); static instance* lua_instance(); diff --git a/backends/maweb.c b/backends/maweb.c index c495512..08156f2 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -72,7 +72,7 @@ static maweb_command_key cmdline_keys[] = { {"GO_MINUS_SMALL", 50}, {"PAUSE_SMALL", 51}, {"GO_PLUS_SMALL", 52} }; -int init(){ +MM_PLUGIN_API int init(){ backend maweb = { .name = BACKEND_NAME, .conf = maweb_configure, diff --git a/backends/maweb.h b/backends/maweb.h index 14e4755..9091cda 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -1,6 +1,6 @@ #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int maweb_configure(char* option, char* value); static int maweb_configure_instance(instance* inst, char* option, char* value); static instance* maweb_instance(); diff --git a/backends/midi.c b/backends/midi.c index cde278b..f380f59 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -23,7 +23,7 @@ static struct { .detect = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend midi = { .name = BACKEND_NAME, .conf = midi_configure, diff --git a/backends/midi.h b/backends/midi.h index 6c3fcf9..b9934f1 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -1,6 +1,6 @@ #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int midi_configure(char* option, char* value); static int midi_configure_instance(instance* instance, char* option, char* value); static instance* midi_instance(); diff --git a/backends/ola.cpp b/backends/ola.cpp index 632cef7..d069a8c 100644 --- a/backends/ola.cpp +++ b/backends/ola.cpp @@ -11,7 +11,7 @@ static ola::io::SelectServer* ola_select = NULL; static ola::OlaCallbackClient* ola_client = NULL; -int init(){ +MM_PLUGIN_API int init(){ backend ola = { .name = BACKEND_NAME, .conf = ola_configure, diff --git a/backends/ola.h b/backends/ola.h index c943d52..1637495 100644 --- a/backends/ola.h +++ b/backends/ola.h @@ -4,7 +4,7 @@ extern "C" { #undef min #undef max - int init(); + MM_PLUGIN_API int init(); static int ola_configure(char* option, char* value); static int ola_configure_instance(instance* instance, char* option, char* value); static instance* ola_instance(); diff --git a/backends/osc.c b/backends/osc.c index bffbba8..d9f9139 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -19,7 +19,7 @@ static struct { .detect = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend osc = { .name = BACKEND_NAME, .conf = osc_configure, diff --git a/backends/osc.h b/backends/osc.h index dd5afb0..86be285 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -7,7 +7,7 @@ #define OSC_RECV_BUF 8192 #define OSC_XMIT_BUF 8192 -int init(); +MM_PLUGIN_API int init(); static int osc_configure(char* option, char* value); static int osc_configure_instance(instance* inst, char* option, char* value); static instance* osc_instance(); diff --git a/backends/sacn.c b/backends/sacn.c index 6e1b20b..d8b3eb3 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -31,7 +31,7 @@ static struct /*_sacn_global_config*/ { .last_announce = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend sacn = { .name = BACKEND_NAME, .conf = sacn_configure, diff --git a/backends/sacn.h b/backends/sacn.h index 7af2a36..631d3a4 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -1,6 +1,6 @@ #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int sacn_configure(char* option, char* value); static int sacn_configure_instance(instance* instance, char* option, char* value); static instance* sacn_instance(); diff --git a/backends/winmidi.c b/backends/winmidi.c index ffca3b4..bda5401 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -23,7 +23,7 @@ static struct { //TODO receive feedback socket until EAGAIN -int init(){ +MM_PLUGIN_API int init(){ backend winmidi = { .name = BACKEND_NAME, .conf = winmidi_configure, diff --git a/backends/winmidi.h b/backends/winmidi.h index e4abda1..ffa6a26 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -1,6 +1,6 @@ #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int winmidi_configure(char* option, char* value); static int winmidi_configure_instance(instance* inst, char* option, char* value); static instance* winmidi_instance(); diff --git a/midimonster.h b/midimonster.h index 3922b03..1192d6a 100644 --- a/midimonster.h +++ b/midimonster.h @@ -5,6 +5,7 @@ #include #include +/* API call attributes and visibilities */ #ifndef MM_API #ifdef _WIN32 #define MM_API __attribute__((dllimport)) @@ -13,6 +14,15 @@ #endif #endif +/* Some build systems may apply the -fvisibility=hidden parameter from the core build to the backends, so mark the init function visible */ +#ifndef MM_PLUGIN_API + #ifdef _WIN32 + #define MM_PLUGIN_API __attribute__((dllexport)) + #else + #define MM_PLUGIN_API __attribute__((visibility ("default"))) + #endif +#endif + /* Straight-forward min / max macros */ #define max(a,b) (((a) > (b)) ? (a) : (b)) #define min(a,b) (((a) < (b)) ? (a) : (b)) -- cgit v1.2.3 From 0b72a1885f064666f9c7369e1d604feeb4b83d66 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Dec 2019 01:23:35 +0100 Subject: Do not build the jack backend in debug mode --- backends/jack.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/backends/jack.c b/backends/jack.c index e8a63bc..192aab2 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -4,8 +4,6 @@ #include #include -#define DEBUG - #include "jack.h" #include #include -- cgit v1.2.3 From bb6e54e99b86a71fcc300890b41b49209245ac61 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Dec 2019 21:48:25 +0100 Subject: Have the configuration file be installed in a separate directory --- LICENSE.txt | 2 +- Makefile | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 3d2ec64..95db371 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2017, cbdev/FJS +Copyright (c) 2017, cbdev/Fabian J. Stumpf All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/Makefile b/Makefile index caad174..8b2d7ec 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ install: install -d "$(DESTDIR)$(PREFIX)/share/midimonster" install -m 0644 configs/* "$(DESTDIR)$(PREFIX)/share/midimonster" ifdef DEFAULT_CFG - install -m 0644 monster.cfg "$(DESTDIR)$(DEFAULT_CFG)" + install -Dm 0644 monster.cfg "$(DESTDIR)$(DEFAULT_CFG)" endif sanitize: export CC = clang diff --git a/README.md b/README.md index 83cd06b..bcae9dd 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ For system-wide install or packaging builds, the following steps are recommended ``` export PREFIX=/usr export PLUGINS=$PREFIX/lib/midimonster -export DEFAULT_CFG=/etc/midimonster.cfg +export DEFAULT_CFG=/etc/midimonster/midimonster.cfg make clean make full make install -- cgit v1.2.3 From 8979c52e767791f97f462835a9e4c5e66d1112cc Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 4 Dec 2019 22:53:27 +0100 Subject: Remove check for executable bit on shared libraries --- plugin.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugin.c b/plugin.c index a14baff..c7c9812 100644 --- a/plugin.c +++ b/plugin.c @@ -141,10 +141,6 @@ load_done: continue; } - if(!(file_stat.st_mode & S_IXUSR)){ - continue; - } - if(plugin_attach(path, entry->d_name)){ goto load_done; } -- cgit v1.2.3 From 9c37eddad24eb7e9bbc9aae723b3a992ec5b4c97 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 5 Dec 2019 19:31:14 +0100 Subject: Unify midi parsing/deparsing code --- backends/jack.c | 8 ++--- backends/winmidi.c | 89 ++++++++++++++++-------------------------------------- backends/winmidi.h | 10 +++--- midimonster.c | 2 +- 4 files changed, 36 insertions(+), 73 deletions(-) diff --git a/backends/jack.c b/backends/jack.c index 192aab2..926f800 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -98,20 +98,20 @@ static int mmjack_process_midi(instance* inst, mmjack_port* port, size_t nframes //ident.fields.port set on output in mmjack_handle_midi ident.fields.sub_channel = event.buffer[0] & 0x0F; ident.fields.sub_type = event.buffer[0] & 0xF0; + ident.fields.sub_control = event.buffer[1]; + value = event.buffer[2]; if(ident.fields.sub_type == 0x80){ ident.fields.sub_type = midi_note; value = 0; } else if(ident.fields.sub_type == midi_pitchbend){ + ident.fields.sub_control = 0; value = event.buffer[1] | (event.buffer[2] << 7); } else if(ident.fields.sub_type == midi_aftertouch){ + ident.fields.sub_control = 0; value = event.buffer[1]; } - else{ - ident.fields.sub_control = event.buffer[1]; - value = event.buffer[2]; - } //append midi data mmjack_midiqueue_append(port, ident, value); } diff --git a/backends/winmidi.c b/backends/winmidi.c index bda5401..b274c06 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -203,35 +203,17 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v for(u = 0; u < num; u++){ ident.label = c[u]->ident; - switch(ident.fields.type){ - case note: - output.components.status = 0x90 | ident.fields.channel; - output.components.data1 = ident.fields.control; - output.components.data2 = v[u].normalised * 127.0; - break; - case cc: - output.components.status = 0xB0 | ident.fields.channel; - output.components.data1 = ident.fields.control; - output.components.data2 = v[u].normalised * 127.0; - break; - case pressure: - output.components.status = 0xA0 | ident.fields.channel; - output.components.data1 = ident.fields.control; - output.components.data2 = v[u].normalised * 127.0; - break; - case aftertouch: - output.components.status = 0xD0 | ident.fields.channel; - output.components.data1 = v[u].normalised * 127.0; - output.components.data2 = 0; - break; - case pitchbend: - output.components.status = 0xE0 | ident.fields.channel; - output.components.data1 = ((int)(v[u].normalised * 16384.0)) & 0x7F; - output.components.data2 = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; - break; - default: - fprintf(stderr, "Unknown winmidi channel type %d\n", ident.fields.type); - continue; + //build output message + output.components.status = ident.fields.type | ident.fields.channel; + output.components.data1 = ident.fields.control; + output.components.data2 = v[u].normalised * 127.0; + if(ident.fields.type == pitchbend){ + output.components.data1 = ((int)(v[u].normalised * 16384.0)) & 0x7F; + output.components.data2 = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; + } + else if(ident.fields.type == aftertouch){ + output.components.data1 = v[u].normalised * 127.0; + output.components.data2 = 0; } midiOutShortMsg(data->device_out, output.dword); @@ -330,40 +312,21 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW //param1 has the message input.dword = param1; ident.fields.channel = input.components.status & 0x0F; - switch(input.components.status & 0xF0){ - case 0x80: - ident.fields.type = note; - ident.fields.control = input.components.data1; - val.normalised = 0.0; - break; - case 0x90: - ident.fields.type = note; - ident.fields.control = input.components.data1; - val.normalised = (double) input.components.data2 / 127.0; - break; - case 0xA0: - ident.fields.type = pressure; - ident.fields.control = input.components.data1; - val.normalised = (double) input.components.data2 / 127.0; - break; - case 0xB0: - ident.fields.type = cc; - ident.fields.control = input.components.data1; - val.normalised = (double) input.components.data2 / 127.0; - break; - case 0xD0: - ident.fields.type = aftertouch; - ident.fields.control = 0; - val.normalised = (double) input.components.data1 / 127.0; - break; - case 0xE0: - ident.fields.type = pitchbend; - ident.fields.control = 0; - val.normalised = (double)((input.components.data2 << 7) | input.components.data1) / 16384.0; - break; - default: - fprintf(stderr, "winmidi unhandled status byte %02X\n", input.components.status); - return; + ident.fields.type = input.components.status & 0xF0; + ident.fields.control = input.components.data1; + val.normalised = (double) input.components.data2 / 127.0; + + if(ident.fields.type == 0x80){ + ident.fields.type = note; + val.normalised = 0; + } + else if(ident.fields.type == pitchbend){ + ident.fields.control = 0; + val.normalised = (double)((input.components.data2 << 7) | input.components.data1) / 16384.0; + } + else if(ident.fields.type == aftertouch){ + ident.fields.control = 0; + val.normalised = (double) input.components.data1 / 127.0; } break; case MIM_LONGDATA: diff --git a/backends/winmidi.h b/backends/winmidi.h index ffa6a26..8c2d76b 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -19,11 +19,11 @@ typedef struct /*_winmidi_instance_data*/ { enum /*_winmidi_channel_type*/ { none = 0, - note, - cc, - pressure, - aftertouch, - pitchbend + note = 0x90, + cc = 0xB0, + pressure = 0xA0, + aftertouch = 0xD0, + pitchbend = 0xE0 }; typedef union { diff --git a/midimonster.c b/midimonster.c index eb64974..e6c0842 100644 --- a/midimonster.c +++ b/midimonster.c @@ -224,7 +224,7 @@ static void event_free(){ } static int usage(char* fn){ - fprintf(stderr, "MIDIMonster v0.2\n"); + fprintf(stderr, "MIDIMonster v0.3\n"); fprintf(stderr, "Usage:\n"); fprintf(stderr, "\t%s \n", fn); return EXIT_FAILURE; -- cgit v1.2.3 From d8d636901b43a4b94bc54052b3fb26517d48e055 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 5 Dec 2019 19:48:40 +0100 Subject: Create basic manpage (Fixes #38) --- midimonster.1 | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 midimonster.1 diff --git a/midimonster.1 b/midimonster.1 new file mode 100644 index 0000000..8e97109 --- /dev/null +++ b/midimonster.1 @@ -0,0 +1,18 @@ +.TH MIDIMONSTER 8 "December 2019" +.SH NAME +midimonster \- Multi-protocol translation tool +.SH SYNOPSIS +.B midimonster +.I config-file +.SH DESCRIPTION +.B MIDIMonster +allows the user to translate any channel on one supported protocol into channel(s) +on any other (or the same) supported protocol. +.SH OPTIONS +.TP +.I config-file +The configuration file to read. If not specified, a default configuration file is read. +.SH "SEE ALSO" +Online documentation and repository at https://github.com/cbdevnet/midimonster +.SH AUTHOR +Fabian "cbdev" Stumpf -- cgit v1.2.3 From 3eada28582b144519e95a44ee3adc3f46d39036e Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 5 Dec 2019 21:05:14 +0100 Subject: Add flags parameter to channel parser plugin API (Fixes #31) --- backends/artnet.c | 2 +- backends/artnet.h | 2 +- backends/evdev.c | 2 +- backends/evdev.h | 2 +- backends/jack.c | 2 +- backends/jack.h | 2 +- backends/loopback.c | 2 +- backends/loopback.h | 2 +- backends/lua.c | 2 +- backends/lua.h | 2 +- backends/maweb.c | 2 +- backends/maweb.h | 2 +- backends/midi.c | 2 +- backends/midi.h | 2 +- backends/ola.cpp | 2 +- backends/ola.h | 2 +- backends/osc.c | 2 +- backends/osc.h | 2 +- backends/sacn.c | 2 +- backends/sacn.h | 2 +- backends/winmidi.c | 2 +- backends/winmidi.h | 2 +- config.c | 8 ++++---- midimonster.h | 15 ++++++++++++--- 24 files changed, 38 insertions(+), 29 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index 8a62a43..57eb7b1 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -156,7 +156,7 @@ static int artnet_configure_instance(instance* inst, char* option, char* value){ return 1; } -static channel* artnet_channel(instance* inst, char* spec){ +static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ artnet_instance_data* data = (artnet_instance_data*) inst->impl; char* spec_next = spec; unsigned chan_a = strtoul(spec, &spec_next, 10); diff --git a/backends/artnet.h b/backends/artnet.h index cce11d1..f6a6709 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -7,7 +7,7 @@ MM_PLUGIN_API int init(); static int artnet_configure(char* option, char* value); static int artnet_configure_instance(instance* instance, char* option, char* value); static instance* artnet_instance(); -static channel* artnet_channel(instance* instance, char* spec); +static channel* artnet_channel(instance* instance, char* spec, uint8_t flags); static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v); static int artnet_handle(size_t num, managed_fd* fds); static int artnet_start(); diff --git a/backends/evdev.c b/backends/evdev.c index dd2231b..0da5ae6 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -249,7 +249,7 @@ static int evdev_configure_instance(instance* inst, char* option, char* value) { return 1; } -static channel* evdev_channel(instance* inst, char* spec){ +static channel* evdev_channel(instance* inst, char* spec, uint8_t flags){ #ifndef EVDEV_NO_UINPUT evdev_instance_data* data = (evdev_instance_data*) inst->impl; #endif diff --git a/backends/evdev.h b/backends/evdev.h index 30ce892..6504416 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -12,7 +12,7 @@ MM_PLUGIN_API int init(); static int evdev_configure(char* option, char* value); static int evdev_configure_instance(instance* instance, char* option, char* value); static instance* evdev_instance(); -static channel* evdev_channel(instance* instance, char* spec); +static channel* evdev_channel(instance* instance, char* spec, uint8_t flags); static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v); static int evdev_handle(size_t num, managed_fd* fds); static int evdev_start(); diff --git a/backends/jack.c b/backends/jack.c index 926f800..e7bed04 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -409,7 +409,7 @@ static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){ return 0; } -static channel* mmjack_channel(instance* inst, char* spec){ +static channel* mmjack_channel(instance* inst, char* spec, uint8_t flags){ mmjack_instance_data* data = (mmjack_instance_data*) inst->impl; mmjack_channel_ident ident = { .label = 0 diff --git a/backends/jack.h b/backends/jack.h index 5598042..a7f3e8b 100644 --- a/backends/jack.h +++ b/backends/jack.h @@ -6,7 +6,7 @@ MM_PLUGIN_API int init(); static int mmjack_configure(char* option, char* value); static int mmjack_configure_instance(instance* inst, char* option, char* value); static instance* mmjack_instance(); -static channel* mmjack_channel(instance* inst, char* spec); +static channel* mmjack_channel(instance* inst, char* spec, uint8_t flags); static int mmjack_set(instance* inst, size_t num, channel** c, channel_value* v); static int mmjack_handle(size_t num, managed_fd* fds); static int mmjack_start(); diff --git a/backends/loopback.c b/backends/loopback.c index 0a45bde..41e6f85 100644 --- a/backends/loopback.c +++ b/backends/loopback.c @@ -49,7 +49,7 @@ static instance* loopback_instance(){ return i; } -static channel* loopback_channel(instance* inst, char* spec){ +static channel* loopback_channel(instance* inst, char* spec, uint8_t flags){ size_t u; loopback_instance_data* data = (loopback_instance_data*) inst->impl; diff --git a/backends/loopback.h b/backends/loopback.h index a08417b..ee51c66 100644 --- a/backends/loopback.h +++ b/backends/loopback.h @@ -4,7 +4,7 @@ MM_PLUGIN_API int init(); static int loopback_configure(char* option, char* value); static int loopback_configure_instance(instance* inst, char* option, char* value); static instance* loopback_instance(); -static channel* loopback_channel(instance* inst, char* spec); +static channel* loopback_channel(instance* inst, char* spec, uint8_t flags); static int loopback_set(instance* inst, size_t num, channel** c, channel_value* v); static int loopback_handle(size_t num, managed_fd* fds); static int loopback_start(); diff --git a/backends/lua.c b/backends/lua.c index 0b47b2c..40e6613 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -330,7 +330,7 @@ static instance* lua_instance(){ return inst; } -static channel* lua_channel(instance* inst, char* spec){ +static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ size_t u; lua_instance_data* data = (lua_instance_data*) inst->impl; diff --git a/backends/lua.h b/backends/lua.h index e187a8e..4ea5b0a 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -13,7 +13,7 @@ MM_PLUGIN_API int init(); static int lua_configure(char* option, char* value); static int lua_configure_instance(instance* inst, char* option, char* value); static instance* lua_instance(); -static channel* lua_channel(instance* inst, char* spec); +static channel* lua_channel(instance* inst, char* spec, uint8_t flags); static int lua_set(instance* inst, size_t num, channel** c, channel_value* v); static int lua_handle(size_t num, managed_fd* fds); static int lua_start(); diff --git a/backends/maweb.c b/backends/maweb.c index 08156f2..d008cc0 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -229,7 +229,7 @@ static instance* maweb_instance(){ return inst; } -static channel* maweb_channel(instance* inst, char* spec){ +static channel* maweb_channel(instance* inst, char* spec, uint8_t flags){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; maweb_channel_data chan = { 0 diff --git a/backends/maweb.h b/backends/maweb.h index 9091cda..05095f8 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -4,7 +4,7 @@ MM_PLUGIN_API int init(); static int maweb_configure(char* option, char* value); static int maweb_configure_instance(instance* inst, char* option, char* value); static instance* maweb_instance(); -static channel* maweb_channel(instance* inst, char* spec); +static channel* maweb_channel(instance* inst, char* spec, uint8_t flags); static int maweb_set(instance* inst, size_t num, channel** c, channel_value* v); static int maweb_handle(size_t num, managed_fd* fds); static int maweb_start(); diff --git a/backends/midi.c b/backends/midi.c index f380f59..92776ca 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -110,7 +110,7 @@ static int midi_configure_instance(instance* inst, char* option, char* value){ return 1; } -static channel* midi_channel(instance* inst, char* spec){ +static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ midi_channel_ident ident = { .label = 0 }; diff --git a/backends/midi.h b/backends/midi.h index b9934f1..4e16f90 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -4,7 +4,7 @@ MM_PLUGIN_API int init(); static int midi_configure(char* option, char* value); static int midi_configure_instance(instance* instance, char* option, char* value); static instance* midi_instance(); -static channel* midi_channel(instance* instance, char* spec); +static channel* midi_channel(instance* instance, char* spec, uint8_t flags); static int midi_set(instance* inst, size_t num, channel** c, channel_value* v); static int midi_handle(size_t num, managed_fd* fds); static int midi_start(); diff --git a/backends/ola.cpp b/backends/ola.cpp index d069a8c..c13e8f9 100644 --- a/backends/ola.cpp +++ b/backends/ola.cpp @@ -68,7 +68,7 @@ static int ola_configure_instance(instance* inst, char* option, char* value){ return 1; } -static channel* ola_channel(instance* inst, char* spec){ +static channel* ola_channel(instance* inst, char* spec, uint8_t flags){ ola_instance_data* data = (ola_instance_data*) inst->impl; char* spec_next = spec; unsigned chan_a = strtoul(spec, &spec_next, 10); diff --git a/backends/ola.h b/backends/ola.h index 1637495..0c42bac 100644 --- a/backends/ola.h +++ b/backends/ola.h @@ -8,7 +8,7 @@ extern "C" { static int ola_configure(char* option, char* value); static int ola_configure_instance(instance* instance, char* option, char* value); static instance* ola_instance(); - static channel* ola_channel(instance* instance, char* spec); + static channel* ola_channel(instance* instance, char* spec, uint8_t flags); static int ola_set(instance* inst, size_t num, channel** c, channel_value* v); static int ola_handle(size_t num, managed_fd* fds); static int ola_start(); diff --git a/backends/osc.c b/backends/osc.c index d9f9139..757ad89 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -577,7 +577,7 @@ static instance* osc_instance(){ return inst; } -static channel* osc_map_channel(instance* inst, char* spec){ +static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags){ size_t u, p; osc_instance_data* data = (osc_instance_data*) inst->impl; osc_channel_ident ident = { diff --git a/backends/osc.h b/backends/osc.h index 86be285..6f3b923 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -11,7 +11,7 @@ MM_PLUGIN_API int init(); static int osc_configure(char* option, char* value); static int osc_configure_instance(instance* inst, char* option, char* value); static instance* osc_instance(); -static channel* osc_map_channel(instance* inst, char* spec); +static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags); static int osc_set(instance* inst, size_t num, channel** c, channel_value* v); static int osc_handle(size_t num, managed_fd* fds); static int osc_start(); diff --git a/backends/sacn.c b/backends/sacn.c index d8b3eb3..2229b8a 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -198,7 +198,7 @@ static instance* sacn_instance(){ return inst; } -static channel* sacn_channel(instance* inst, char* spec){ +static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ sacn_instance_data* data = (sacn_instance_data*) inst->impl; char* spec_next = spec; diff --git a/backends/sacn.h b/backends/sacn.h index 631d3a4..1d3268c 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -4,7 +4,7 @@ MM_PLUGIN_API int init(); static int sacn_configure(char* option, char* value); static int sacn_configure_instance(instance* instance, char* option, char* value); static instance* sacn_instance(); -static channel* sacn_channel(instance* instance, char* spec); +static channel* sacn_channel(instance* instance, char* spec, uint8_t flags); static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v); static int sacn_handle(size_t num, managed_fd* fds); static int sacn_start(); diff --git a/backends/winmidi.c b/backends/winmidi.c index b274c06..790257b 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -110,7 +110,7 @@ static instance* winmidi_instance(){ return i; } -static channel* winmidi_channel(instance* inst, char* spec){ +static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ char* next_token = NULL; winmidi_channel_ident ident = { .label = 0 diff --git a/backends/winmidi.h b/backends/winmidi.h index 8c2d76b..985c46a 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -4,7 +4,7 @@ MM_PLUGIN_API int init(); static int winmidi_configure(char* option, char* value); static int winmidi_configure_instance(instance* inst, char* option, char* value); static instance* winmidi_instance(); -static channel* winmidi_channel(instance* inst, char* spec); +static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags); static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int winmidi_handle(size_t num, managed_fd* fds); static int winmidi_start(); diff --git a/config.c b/config.c index 8e7e581..0b9173e 100644 --- a/config.c +++ b/config.c @@ -175,7 +175,7 @@ static int config_glob_scan(instance* inst, channel_spec* spec){ return 0; } -static channel* config_glob_resolve(instance* inst, channel_spec* spec, uint64_t n){ +static channel* config_glob_resolve(instance* inst, channel_spec* spec, uint64_t n, uint8_t map_direction){ size_t glob = 0, glob_length; ssize_t bytes = 0; uint64_t current_value = 0; @@ -216,7 +216,7 @@ static channel* config_glob_resolve(instance* inst, channel_spec* spec, uint64_t } } - result = inst->backend->channel(inst, resolved_spec); + result = inst->backend->channel(inst, resolved_spec, map_direction); if(spec->globs && !result){ fprintf(stderr, "Failed to match multichannel evaluation %s to a channel\n", resolved_spec); } @@ -294,8 +294,8 @@ static int config_map(char* to_raw, char* from_raw){ //iterate, resolve globs and map rv = 0; for(n = 0; !rv && n < max(spec_from.channels, spec_to.channels); n++){ - channel_from = config_glob_resolve(instance_from, &spec_from, min(n, spec_from.channels)); - channel_to = config_glob_resolve(instance_to, &spec_to, min(n, spec_to.channels)); + channel_from = config_glob_resolve(instance_from, &spec_from, min(n, spec_from.channels), mmchannel_input); + channel_to = config_glob_resolve(instance_to, &spec_to, min(n, spec_to.channels), mmchannel_output); if(!channel_from || !channel_to){ rv = 1; diff --git a/midimonster.h b/midimonster.h index 1192d6a..5ce0c73 100644 --- a/midimonster.h +++ b/midimonster.h @@ -87,8 +87,11 @@ struct _managed_fd; * Parse instance configuration from the user-supplied configuration * file. Returning a non-zero value fails config parsing. * * mmbackend_channel - * Parse a channel-spec to be mapped to/from. Returning NULL signals an - * out-of-memory condition and terminates the program. + * Parse a channel-spec to be mapped to/from. The `falgs` parameter supplies + * additional information to the parser, such as whether the channel is being + * queried for use as input (to the MIDIMonster core) and/or output + * (from the MIDIMonster core) channel (on a per-query basis). + * Returning NULL signals an out-of-memory condition and terminates the program. * * mmbackend_start * Called after all instances have been created and all mappings * have been set up. Only backends for which instances have been configured @@ -121,7 +124,7 @@ struct _managed_fd; */ typedef int (*mmbackend_handle_event)(struct _backend_instance* inst, size_t channels, struct _backend_channel** c, struct _channel_value* v); typedef struct _backend_instance* (*mmbackend_create_instance)(); -typedef struct _backend_channel* (*mmbackend_parse_channel)(struct _backend_instance* instance, char* spec); +typedef struct _backend_channel* (*mmbackend_parse_channel)(struct _backend_instance* instance, char* spec, uint8_t flags); typedef void (*mmbackend_free_channel)(struct _backend_channel* c); typedef int (*mmbackend_configure)(char* option, char* value); typedef int (*mmbackend_configure_instance)(struct _backend_instance* instance, char* option, char* value); @@ -130,6 +133,12 @@ typedef int (*mmbackend_start)(); typedef uint32_t (*mmbackend_interval)(); typedef int (*mmbackend_shutdown)(); +/* Bit masks for the `flags` parameter to mmbackend_parse_channel */ +typedef enum { + mmchannel_input = 0x1, + mmchannel_output = 0x2 +} mmbe_channel_flags; + /* Channel event value, .normalised is used by backends to determine channel values */ typedef struct _channel_value { union { -- cgit v1.2.3 From d92cf08ac473b2a904ba8873a7c7358e5440cb39 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 6 Dec 2019 18:11:45 +0100 Subject: Fix manpage section --- midimonster.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/midimonster.1 b/midimonster.1 index 8e97109..131ed44 100644 --- a/midimonster.1 +++ b/midimonster.1 @@ -1,4 +1,4 @@ -.TH MIDIMONSTER 8 "December 2019" +.TH MIDIMONSTER 1 "December 2019" .SH NAME midimonster \- Multi-protocol translation tool .SH SYNOPSIS -- cgit v1.2.3 From 96fe966928cd83f22aed388a11013b67c1a374a1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 6 Dec 2019 18:58:59 +0100 Subject: Make install path for examples configurable --- Makefile | 5 +++-- README.md | 13 +++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 8b2d7ec..8dab638 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ OBJS = config.o backend.o plugin.o PREFIX ?= /usr PLUGIN_INSTALL = $(PREFIX)/lib/midimonster +EXAMPLES ?= $(PREFIX)/share/midimonster SYSTEM := $(shell uname -s) CFLAGS ?= -g -Wall -Wpedantic @@ -68,8 +69,8 @@ install: install -m 0755 midimonster "$(DESTDIR)$(PREFIX)/bin" install -d "$(DESTDIR)$(PLUGIN_INSTALL)" install -m 0755 backends/*.so "$(DESTDIR)$(PLUGIN_INSTALL)" - install -d "$(DESTDIR)$(PREFIX)/share/midimonster" - install -m 0644 configs/* "$(DESTDIR)$(PREFIX)/share/midimonster" + install -d "$(DESTDIR)$(EXAMPLES)" + install -m 0644 configs/* "$(DESTDIR)$(EXAMPLES)" ifdef DEFAULT_CFG install -Dm 0644 monster.cfg "$(DESTDIR)$(DEFAULT_CFG)" endif diff --git a/README.md b/README.md index bcae9dd..9bcd913 100644 --- a/README.md +++ b/README.md @@ -159,12 +159,13 @@ For Linux and OSX, just running `make` in the source directory should do the tri The build process accepts the following parameters, either from the environment or as arguments to the `make` invocation: -| Target | Parameter | Default value | Description | -|---------------|-----------------------|-------------------------------|-------------------------------| -| build targets | `DEFAULT_CFG` | `monster.cfg` | Default configuration file | -| build targets | `PLUGINS` | Linux/OSX: `./backends/`, Windows: `backends\` | Backend plugin library path | -| `install` | `DESTDIR` | empty | Destination directory for packaging builds | -| `install` | `PREFIX` | `/usr` | Install prefix for binaries | +| Target | Parameter | Default value | Description | +|-------------------------------|-----------------------|-------------------------------|-------------------------------| +| build targets, `install` | `DEFAULT_CFG` | `monster.cfg` | Default configuration file | +| build targets, `install` | `PLUGINS` | Linux/OSX: `./backends/`, Windows: `backends\` | Backend plugin library path | +| `install` | `DESTDIR` | empty | Destination directory for packaging builds | +| `install` | `PREFIX` | `/usr` | Install prefix for binaries | +| `install` | `EXAMPLES` | `$(PREFIX)/share/midimonster` | Install path for example configurations | Some backends have been marked as optional as they require rather large additional software to be installed, for example the `ola` backend. To create a build including these, run `make full`. -- cgit v1.2.3 From 0468b5c5fafe0b6d72d8cd05613da3e705fa25c3 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 6 Dec 2019 19:05:41 +0100 Subject: Update the build parameters section with some explanations --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9bcd913..c7fd3c1 100644 --- a/README.md +++ b/README.md @@ -159,13 +159,19 @@ For Linux and OSX, just running `make` in the source directory should do the tri The build process accepts the following parameters, either from the environment or as arguments to the `make` invocation: -| Target | Parameter | Default value | Description | -|-------------------------------|-----------------------|-------------------------------|-------------------------------| -| build targets, `install` | `DEFAULT_CFG` | `monster.cfg` | Default configuration file | -| build targets, `install` | `PLUGINS` | Linux/OSX: `./backends/`, Windows: `backends\` | Backend plugin library path | -| `install` | `DESTDIR` | empty | Destination directory for packaging builds | -| `install` | `PREFIX` | `/usr` | Install prefix for binaries | -| `install` | `EXAMPLES` | `$(PREFIX)/share/midimonster` | Install path for example configurations | +| Target | Parameter | Default value | Description | +|---------------|-----------------------|-------------------------------|-------------------------------| +| build targets | `DEFAULT_CFG` | `monster.cfg` | Default configuration file | +| build targets | `PLUGINS` | Linux/OSX: `./backends/`, Windows: `backends\` | Backend plugin library path | +| `install` | `PREFIX` | `/usr` | Install prefix for binaries | +| `install` | `DESTDIR` | empty | Destination directory for packaging builds | +| `install` | `DEFAULT_CFG` | empty | Install path for default configuration file | +| `install` | `PLUGINS` | `$(PREFIX)/lib/midimonster` | Destination directory for packaging builds | +| `install` | `EXAMPLES` | `$(PREFIX)/share/midimonster` | Install path for example configurations | + +Note that the same variables may have different default values depending on the target. This implies that +builds that are destined to be installed require those variables to be set to the same value for the +build and `install` targets. Some backends have been marked as optional as they require rather large additional software to be installed, for example the `ola` backend. To create a build including these, run `make full`. -- cgit v1.2.3 From 01c8635205e25ba2e6c128010ad512cf03868bc2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 6 Dec 2019 19:07:25 +0100 Subject: Fix variable description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c7fd3c1..785d741 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ as arguments to the `make` invocation: | `install` | `PREFIX` | `/usr` | Install prefix for binaries | | `install` | `DESTDIR` | empty | Destination directory for packaging builds | | `install` | `DEFAULT_CFG` | empty | Install path for default configuration file | -| `install` | `PLUGINS` | `$(PREFIX)/lib/midimonster` | Destination directory for packaging builds | +| `install` | `PLUGINS` | `$(PREFIX)/lib/midimonster` | Install path for backend shared objects | | `install` | `EXAMPLES` | `$(PREFIX)/share/midimonster` | Install path for example configurations | Note that the same variables may have different default values depending on the target. This implies that -- cgit v1.2.3 From 2cb91753b2cab927fca9107128519872ac70665d Mon Sep 17 00:00:00 2001 From: Spacelord Date: Fri, 6 Dec 2019 19:58:16 +0100 Subject: Initial installer release --- installer.sh | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 installer.sh diff --git a/installer.sh b/installer.sh new file mode 100644 index 0000000..fa898fb --- /dev/null +++ b/installer.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +################################################ SETUP ################################################ +deps=(libasound2-dev libevdev-dev liblua5.3-dev libola-dev libjack-jackd2-dev pkg-config libssl-dev gcc make wget git) +user=$(whoami) # for bypassing user check replace "$(whoami)" with "root". + +script_path="`cd $0; pwd`" # Script dir +tmp_path=$(mktemp -d) # Repo download path + +Iversion="v0.2" # (fallback version if ) +makeargs=full # 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" + +################################################ SETUP ################################################ + +############################################## 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 + echo "Installing "$t""; + apt-get install $t -Y; + echo "Done."; + else + echo ""$t" already installed!" + + fi +done +echo "" +} + +INSTALL-PREP () { + echo "Starting Git!" + git clone https://github.com/cbdevnet/midimonster.git "$tmp_path" # Gets Midimonster + Iversion=(git describe --abbrev=0) # Get last tag(stable version) + echo "Starting Git checkout to "$Iversion"" + git checkout $Iversion $tmp_path + + echo "" + + 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}" + + + export PREFIX=$VAR_PREFIX + export PLUGINS=$VAR_PLUGINS + export DEFAULT_CFG=$VAR_DEFAULT_CFG + export DESTDIR=$VAR_DESTDIR + export EXAMPLES=$VAR_EXAMPLE_CFGS +} + +INSTALL-RUN () { # Build + cd "$tmp_path" + make clean + make $makeargs + make install +} + +ERROR () { + echo "Aborting..." + CLEAN + exit 1 +} + +DONE () { + echo Done. + CLEAN + exit 0 +} + +CLEAN () { + echo "Cleaning..." + rm -rf $tmp_path +} + +############################################## FUNCTIONS ############################################## + + +################################################ Main ################################################# + +trap ERROR SIGINT SIGTERM SIGKILL +clear + +if [ $user != "root" ]; then # Check if $user = root! + echo "Installer must be run as root" + ERROR +fi + +if [ $(wget -q --spider http://github.com) $? -eq 0 ]; then "INSTALL-DEPS"; else echo You need connection to the internet; ERROR ; fi + +INSTALL-PREP +echo "" +INSTALL-RUN +DONE \ No newline at end of file -- cgit v1.2.3 From 0e7645ddbd3dfa20ccc246c4aa50a3299fdb708e Mon Sep 17 00:00:00 2001 From: Spacelord Date: Fri, 6 Dec 2019 20:04:56 +0100 Subject: Change $makeargs to "all" --- installer.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installer.sh b/installer.sh index fa898fb..bf2775c 100644 --- a/installer.sh +++ b/installer.sh @@ -8,7 +8,7 @@ script_path="`cd $0; pwd`" # Script dir tmp_path=$(mktemp -d) # Repo download path Iversion="v0.2" # (fallback version if ) -makeargs=full # Build args +makeargs=all # Build args VAR_DESTDIR="" # Unused VAR_PREFIX="/usr" @@ -25,7 +25,7 @@ for t in ${deps[@]}; do if [ $(dpkg-query -W -f='${Status}' $t 2>/dev/null | grep -c "ok installed") -eq 0 ]; then echo "Installing "$t""; - apt-get install $t -Y; + apt-get install $t -y; echo "Done."; else echo ""$t" already installed!" -- cgit v1.2.3 From 980e3d84c3da1be28d6e46d8f78ae1816961eb9d Mon Sep 17 00:00:00 2001 From: Spacelord Date: Fri, 6 Dec 2019 20:09:08 +0100 Subject: Remove ola from deps --- installer.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installer.sh b/installer.sh index bf2775c..6d438a7 100644 --- a/installer.sh +++ b/installer.sh @@ -1,7 +1,7 @@ #!/bin/bash ################################################ SETUP ################################################ -deps=(libasound2-dev libevdev-dev liblua5.3-dev libola-dev libjack-jackd2-dev pkg-config libssl-dev gcc make wget git) +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". script_path="`cd $0; pwd`" # Script dir @@ -25,7 +25,7 @@ for t in ${deps[@]}; do if [ $(dpkg-query -W -f='${Status}' $t 2>/dev/null | grep -c "ok installed") -eq 0 ]; then echo "Installing "$t""; - apt-get install $t -y; + apt-get install $t; echo "Done."; else echo ""$t" already installed!" -- cgit v1.2.3 From f8d34ee6a8de3fe64339a2d1cbce767cbee2e520 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Fri, 6 Dec 2019 22:06:18 +0100 Subject: Fix git checkout error with git init --- installer.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/installer.sh b/installer.sh index 6d438a7..eab9f50 100644 --- a/installer.sh +++ b/installer.sh @@ -40,6 +40,7 @@ INSTALL-PREP () { git clone https://github.com/cbdevnet/midimonster.git "$tmp_path" # Gets Midimonster Iversion=(git describe --abbrev=0) # Get last tag(stable version) echo "Starting Git checkout to "$Iversion"" + git init $tmp_path git checkout $Iversion $tmp_path echo "" -- cgit v1.2.3 From a04b0d13d9248f89b9fe71f9d433ad44aac99116 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Fri, 6 Dec 2019 22:21:44 +0100 Subject: Added update to README --- installer.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 installer.sh diff --git a/installer.sh b/installer.sh old mode 100644 new mode 100755 -- cgit v1.2.3 From 7e59d4832f819abe0bc981ebdac20f6e12a031e7 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Fri, 6 Dec 2019 22:28:04 +0100 Subject: Added update to README --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 785d741..1f22657 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,14 @@ for example ``` make jack.so ``` +#### Buiding with Installer + +For easy installation, the following steps are required: + +``` +wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh ./ +./installer.sh +``` #### Building for packaging or installation -- cgit v1.2.3 From 553633e0ab0191756bb5b8810a461168dfc66a3d Mon Sep 17 00:00:00 2001 From: Spacelord Date: Fri, 6 Dec 2019 22:37:40 +0100 Subject: added chmod +x to Installer README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1f22657..e925541 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ For easy installation, the following steps are required: ``` wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh ./ +chmod +x ./installer.sh ./installer.sh ``` -- cgit v1.2.3 From 34b8dd5c71615724408022db37dcf94576a12280 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 6 Dec 2019 22:51:56 +0100 Subject: Add link to installer section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f22657..9052034 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ make jack.so ``` #### Buiding with Installer -For easy installation, the following steps are required: +For easy installation on Linux, the [installer script](installer.sh) can be used: ``` wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh ./ -- cgit v1.2.3 From 1bb3b9a3eaf94af045c39a1ff1ee8bf9b8e5b8ec Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 7 Dec 2019 20:22:03 +0100 Subject: Mention debianization in README --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 454284f..8d38565 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ for example ``` make jack.so ``` -#### Buiding with Installer +#### Using the installer For easy installation on Linux, the [installer script](installer.sh) can be used: @@ -208,6 +208,9 @@ make install Depending on your configuration of `DESTDIR`, the `make install` step may require root privileges to install the binaries to the appropriate destinations. +To create Debian packages, use the debianization and `git-buildpackage` configuration on the `debian/master` +branch. Simply running `gbp buildpackage` should build a package for the last tagged release. + #### Building for Windows To build for Windows, you still need to compile on a Linux machine (virtual machines work well for this). -- cgit v1.2.3