diff options
66 files changed, 7465 insertions, 1426 deletions
@@ -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 @@ -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 @@ -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 @@ -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 @@ -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); @@ -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(¤t, &data->channel[ident.fields.channel].out[ident.fields.parameter], sizeof(current))){ + //update current value + data->channel[ident.fields.channel].out[ident.fields.parameter] = current; + //mark channel + data->channel[ident.fields.channel].mark = 1; + mark = 1; + } } - - //fix destination rport if required - if(data->forced_rport){ - //cheating a bit because both IPv4 and IPv6 have the port at the same offset - struct sockaddr_in* sockadd = (struct sockaddr_in*) &(data->dest); - sockadd->sin_port = htobe16(data->forced_rport); + + if(mark){ + //output all marked channels + for(evt = 0; !rv && evt < num; evt++){ + ident.label = c[evt]->ident; + if(data->channel[ident.fields.channel].mark){ + rv |= osc_output_channel(inst, ident.fields.channel); + data->channel[ident.fields.channel].mark = 0; + } + } } + return rv; +} - //find all marked channels - for(evt = 0; evt < data->channels; evt++){ - //zero output buffer - memset(xmit_buf, 0, sizeof(xmit_buf)); - if(data->channel[evt].mark){ - //determine minimum packet size - if(osc_align((data->root ? strlen(data->root) : 0) + strlen(data->channel[evt].path) + 1) + osc_align(data->channel[evt].params + 2) >= sizeof(xmit_buf)){ - fprintf(stderr, "Insufficient buffer size for OSC transmitting channel %s.%s\n", inst->name, data->channel[evt].path); - return 1; - } +static int osc_process_packet(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){ + osc_instance_data* data = (osc_instance_data*) inst->impl; + size_t c, p, offset = 0; + osc_parameter_value min, max, cur; + channel_value evt; + osc_channel_ident ident = { + .label = 0 + }; + channel* chan = NULL; - off = 0; - //copy osc target path - if(data->root){ - memcpy(xmit_buf, data->root, strlen(data->root)); - off += strlen(data->root); - } - memcpy(xmit_buf + off, data->channel[evt].path, strlen(data->channel[evt].path)); - off += strlen(data->channel[evt].path) + 1; - off = osc_align(off); - - //get format string offset, initialize - format = xmit_buf + off; - off += osc_align(data->channel[evt].params + 2); - *format = ','; - format++; - - //gather subchannels, unmark - members = 0; - for(p = 0; p < data->channels && members < data->channel[evt].params; p++){ - if(!strcmp(data->channel[evt].path, data->channel[p].path)){ - //unmark channel - data->channel[p].mark = 0; - - //sanity check - if(data->channel[p].param_index >= data->channel[evt].params){ - fprintf(stderr, "OSC channel %s.%s has multiple parameter offset definitions\n", inst->name, data->channel[evt].path); - return 1; - } + if(payload_len % 4){ + fprintf(stderr, "Invalid OSC packet, data length %" 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. @@ -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, ¤t)){ 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 @@ -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 |