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