diff options
author | cbdev <cb@cbcdn.com> | 2020-03-23 00:15:00 +0100 |
---|---|---|
committer | cbdev <cb@cbcdn.com> | 2020-03-23 00:15:00 +0100 |
commit | 37e712edf23a49be5387f945ab9ea57cc0b57f22 (patch) | |
tree | 5037b14bdbd08baa8f104f7e4946aa5643281a94 | |
parent | 666aec036f9bf0de82c435bd7eace271613cee1e (diff) | |
parent | aa02ccf3abf183207b24fcbb9460cbd904a698e2 (diff) | |
download | midimonster-37e712edf23a49be5387f945ab9ea57cc0b57f22.tar.gz midimonster-37e712edf23a49be5387f945ab9ea57cc0b57f22.tar.bz2 midimonster-37e712edf23a49be5387f945ab9ea57cc0b57f22.zip |
Merge current master
55 files changed, 3774 insertions, 1263 deletions
diff --git a/.travis-ci.sh b/.travis-ci.sh index da36c17..c832f2c 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -1,84 +1,113 @@ #!/bin/bash -# This script is triggered from the script section of .travis.yml -# It runs the appropriate commands depending on the task requested. +if [ "$TASK" = "spellcheck" ]; then + result=0 + # Create list of files to be spellchecked + spellcheck_files=$(find . -type f | grep -v ".git/") -set -e + # Run spellintian to find spelling errors + sl_results=$(xargs spellintian 2>&1 <<< "$spellcheck_files") -COVERITY_SCAN_BUILD_URL="https://scan.coverity.com/scripts/travisci_build_coverity_scan.sh" + sl_errors=$(wc -l <<< "$sl_results") + sl_errors_dups=$((grep "\(duplicate word\)" | wc -l) <<< "$sl_results") + sl_errors_nodups=$((grep -v "\(duplicate word\)" | wc -l) <<< "$sl_results") -SPELLINGBLACKLIST=$(cat <<-BLACKLIST - -wholename "./.git/*" -BLACKLIST -) + if [ "$sl_errors" -ne 0 ]; then + printf "Spellintian found %s errors (%s spelling, %s duplicate words):\n\n" "$sl_errors" "$sl_errors_nodups" "$sl_errors_dups" + printf "%s\n\n" "$sl_results" + result=1 + else + printf "Spellintian reports no errors\n" + fi -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" + # Run codespell to find some more + cs_results=$(xargs codespell --quiet 2 <<< "$spellcheck_files" 2>&1) + cs_errors=$(wc -l <<< "$cs_results") + if [ "$cs_errors" -ne 0 ]; then + printf "Codespell found %s errors:\n\n" "$cs_errors" + printf "%s\n\n" "$cs_results" + result=1 + else + printf "Codespell reports no errors\n" + fi + exit "$result" +elif [ "$TASK" = "codesmell" ]; then + result=0 + + if [ -z "$(which lizard)" ]; then + printf "Installing lizard...\n" + pip3 install lizard + fi + + # Run shellcheck for all shell scripts + printf "Running shellcheck...\n" + shell_files="$(find . -type f -iname \*.sh)" + xargs shellcheck -Cnever -s bash <<< "$shell_files" + if [ "$?" -ne "0" ]; then + result=1 + fi + + # Run cloc for some stats + printf "Code statistics:\n\n" + cloc ./ + + # Run lizard for the project + printf "Running lizard for code complexity analysis\n" + lizard ./ + if [ "$?" -ne "0" ]; then + result=1 + fi + + exit "$result" +elif [ "$TASK" = "sanitize" ]; then + # Run sanitized compile + travis_fold start "make_sanitize" + if make sanitize; then + exit "$?" + fi + travis_fold end "make_sanitize" +elif [ "$TASK" = "windows" ]; then + travis_fold start "make_windows" + if make windows; then + exit "$?" + fi + make -C backends lua.dll + travis_fold end "make_windows" + if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then + travis_fold start "deploy_windows" + mkdir ./deployment + mkdir ./deployment/backends + mkdir ./deployment/docs + cp ./midimonster.exe ./deployment/ + cp ./backends/*.dll ./deployment/backends/ + cp ./monster.cfg ./deployment/monster.cfg + cp ./backends/*.md ./deployment/docs/ + cp -r ./configs ./deployment/ + cd ./deployment + zip -r "./midimonster-$(git describe)-windows.zip" "./" + find . ! -iname '*.zip' -delete + travis_fold end "deploy_windows" + fi else - # Otherwise compile as normal - travis_fold start "make" - make full; - travis_fold end "make" + # Otherwise compile as normal + travis_fold start "make" + if make full; then + exit "$?" + fi + travis_fold end "make" + if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then + travis_fold start "deploy_unix" + mkdir ./deployment + mkdir ./deployment/backends + mkdir ./deployment/docs + cp ./midimonster ./deployment/ + cp ./backends/*.so ./deployment/backends/ + cp ./monster.cfg ./deployment/monster.cfg + cp ./backends/*.md ./deployment/docs/ + cp -r ./configs ./deployment/ + cd ./deployment + tar czf "midimonster-$(git describe)-$TRAVIS_OS_NAME.tgz" ./ + find . ! -iname '*.tgz' -delete + travis_fold end "deploy_unix" + fi fi diff --git a/.travis.yml b/.travis.yml index bdaf63a..d7c25b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,17 @@ language: c -# Use the latest Travis images since they are more up to date than the stable release. group: edge +os: linux +dist: bionic before_script: - export -f travis_fold script: - - "bash -ex .travis-ci.sh" + - "bash .travis-ci.sh" addons: apt: packages: &base_build - # This is the absolute minimum for configure to pass - # Non C++ based tasks use it so they can run make builtfiles - ccache packages: &core_build # This is all the bits we need to enable all options @@ -22,7 +21,9 @@ addons: - libola-dev - libjack-jackd2-dev - liblua5.3-dev + - python3-dev - libssl-dev + - lintian packages: &core_build_gpp_latest - *core_build - gcc-8 @@ -33,49 +34,35 @@ addons: packages: &core_build_windows - *core_build - mingw-w64 + packages: &linters + - python3 + - python3-pip + - lintian + - codespell + - shellcheck + - cloc - -matrix: +jobs: 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 + dist: bionic 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 + dist: bionic compiler: gcc env: TASK='compile' addons: apt: packages: - *core_build_gpp_latest - sources: - - ubuntu-toolchain-r-test - os: linux - dist: xenial + dist: bionic compiler: mingw32-gcc env: - TASK='windows' @@ -84,66 +71,50 @@ matrix: apt: packages: - *core_build_windows - sources: - - ubuntu-toolchain-r-test - os: linux - dist: xenial + dist: bionic 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 + - os: osx + osx_image: xcode10.2 + compiler: clang + env: + - TASK='compile' + - os: osx + osx_image: xcode10.2 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 + env: + - TASK='compile' + - os: osx + osx_image: xcode10.2 + compiler: clang + env: + - TASK='sanitize' - os: linux - dist: xenial - env: TASK='spellintian-duplicates' + dist: bionic + env: TASK='codesmell' addons: apt: packages: - - *core_build - - moreutils + - *linters - os: linux - dist: xenial - env: TASK='codespell' + dist: bionic + env: TASK='spellcheck' addons: apt: packages: - - *core_build - - moreutils + - *linters allow_failures: - os: linux - dist: xenial - compiler: gcc - env: TASK='coverity' - - os: linux - dist: xenial - env: TASK='spellintian-duplicates' + dist: bionic + env: TASK='codesmell' - os: linux - dist: xenial - env: TASK='codespell' + dist: bionic + env: TASK='spellcheck' env: global: @@ -151,21 +122,6 @@ env: - 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 @@ -175,19 +131,19 @@ cache: before_cache: - ccache -s # see how many hits ccache got -install: - - if [ "$TASK" = "codespell" ]; then pip install --user git+https://github.com/codespell-project/codespell.git; fi - before_install: +# Travis clones with --branch, which omits tags. Since we use them for the version string at build time, fetch them + - git pull --tags - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl jack; fi +# 'brew install' sometimes returns non-zero for some arcane reason. Executing 'true' resets the exit code and allows Travis to continue building... +# Travis seems to have Python 2.7 installed by default, which for some reason prevents pkg-config from reading python3.pc + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl jack python3; brew link --overwrite python; true; 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 @@ -195,7 +151,24 @@ before_install: - $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 +# Download libraries to link with on Windows + - if [ "$TASK" == "windows" ]; then mkdir libs; wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; mv lua53.dll libs; fi + +notifications: + irc: + channels: + - "irc.hackint.org#midimonster" + on_success: change # default: always + on_failure: always # default: always + nick: mm_ci + use_notice: true -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 +deploy: + provider: releases + file_glob: true + token: $GITHUB_TOKEN + file: ./deployment/* + skip_cleanup: true + draft: true + on: + tags: true diff --git a/MIDIMonster.svg b/MIDIMonster.svg new file mode 100644 index 0000000..7e411dc --- /dev/null +++ b/MIDIMonster.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 525.82 454.06"><defs><style>.cls-1{fill:#c1282d;}.cls-2{fill:#333132;}.cls-3{fill:#fff;}</style></defs><title>Asset 3</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="Logo"><path class="cls-1" d="M261.45,244c-37.35,0-67.74-31-67.74-69.09A70.34,70.34,0,0,1,199.06,148C217,159.44,239,165.66,262,165.66c22.59,0,44.3-6,62.07-17.11a70.34,70.34,0,0,1,5.13,26.36C329.18,213,298.8,244,261.45,244"/><path class="cls-2" d="M350.86,125.56l-8.44-5.37a75.48,75.48,0,0,1-17,18.67c-16.84,12.1-39,19.48-63.42,19.48-25.48,0-48.59-8.05-65.68-21.13a.16.16,0,0,1,0,.07,74.74,74.74,0,0,1-15.08-17.09l-8.45,5.37a85.44,85.44,0,0,0,19.15,21,77.31,77.31,0,0,0-5.35,28.32c0,42.2,33.54,76.41,74.92,76.41s74.91-34.21,74.91-76.41a77.23,77.23,0,0,0-5.18-27.9A85.73,85.73,0,0,0,350.86,125.56ZM261.45,236.67c-33.39,0-60.56-27.71-60.56-61.76A62.79,62.79,0,0,1,203,158.74a129.21,129.21,0,0,0,117,.49,63,63,0,0,1,2,15.68C322,209,294.84,236.67,261.45,236.67Z"/><path class="cls-2" d="M261.45,215.57a21.3,21.3,0,1,1,20.88-21.3,21.12,21.12,0,0,1-20.88,21.3"/><path class="cls-3" d="M261.45,203.2a8.93,8.93,0,1,1,8.75-8.93,8.86,8.86,0,0,1-8.75,8.93"/><path class="cls-2" d="M513.32,296.36H450.2c-1.83-1.55-3.52-2.83-5.08-4-2.38-1.81-4.32-3.49-6.82-5.66a99.24,99.24,0,0,0-9.89-7.91,358.26,358.26,0,0,0,2-40.38,12.4,12.4,0,0,0,3.87.62h72a12.5,12.5,0,0,0,0-25h-72a12.42,12.42,0,0,0-5.31,1.2,8,8,0,0,0-.22-1l.32-.92,0-1.35a167.76,167.76,0,0,0-3.24-29.64c2-2.13,4-4.76,5.36-6l19.27-18.6h68.86c12.53-11,4.89-25-6-25H440.38l-22.65,21.87A165.62,165.62,0,0,0,397.88,118a176.74,176.74,0,0,0,3.31-57.81A169.18,169.18,0,0,0,383.11,0c.42,31.45-5.81,52.76-11.75,66.57-2.67,6.2-5.53,11.47-6.85,16.77A167.28,167.28,0,0,0,158.44,84c-1.49-4.37-3.81-8.85-6-14-5.94-13.81-12.17-35.11-11.75-66.56A169.39,169.39,0,0,0,122.6,63.7a176.69,176.69,0,0,0,2.73,55.13A166,166,0,0,0,106.67,153l-21.23-20.5H12.5c-10.91,0-18.55,14-6,25H75.34l19.27,18.6c1,.93,2.29,2.59,3.73,4.25A168.8,168.8,0,0,0,94.7,212l0,1.35.23.66c0,.09-.08.19-.11.29a12.4,12.4,0,0,0-3.3-.46H19.54a12.5,12.5,0,1,0,0,25H91.49a12.66,12.66,0,0,0,1.64-.12A359.58,359.58,0,0,0,95.32,280c-3.19,2.37-6.83,5.62-7.8,6.46-2.5,2.17-4.44,3.85-6.81,5.66-1.57,1.19-3.25,2.47-5.09,4H12.5a12.5,12.5,0,0,0,0,25H80.39A12.55,12.55,0,0,0,89,317.69,71.81,71.81,0,0,1,95.85,312c1.13-.86,2.32-1.77,3.58-2.78,2.24,13,5.23,27.25,9.19,42.63,3.43,13.29,15.24,38.77,16.46,41.38a.37.37,0,0,1,0,.11,7,7,0,0,0,12.14,1.07l1.59-1.85c5.62-6.47,11.74-13.74,16.91-20.72,1,11.59,2.43,21.73,4.58,28.29,4.18,12.8,24.92,34.81,32.15,42.22a7.19,7.19,0,0,0,5.83,2.95,7,7,0,0,0,6.45-4.3,6.57,6.57,0,0,0,.41-1.42c2-2.91,7.87-9.87,12.28-15.11s7.66-9.17,10.22-12.35c.23.38.46.77.72,1.14,7.24,10.52,20.42,29.07,25.75,36.56a10,10,0,0,0,8.1,4.21h0a10,10,0,0,0,8.11-4.25c5.19-7.35,17.63-25,25.28-35.77.28-.4.53-.81.79-1.21,2.48,3.06,5.64,6.86,9.68,11.67,4.41,5.25,10.25,12.2,12.28,15.11a6.88,6.88,0,0,0,6.86,5.72,7.19,7.19,0,0,0,5.81-2.93c7.22-7.4,28-29.42,32.17-42.24,2.14-6.56,3.6-16.7,4.57-28.29,5.17,7,11.29,14.25,16.91,20.73.74.84,1.29,1.47,1.6,1.84a7,7,0,0,0,12.13-1.07l.05-.11c1.22-2.6,13-28.09,16.46-41.38,4.11-15.95,7.17-30.66,9.45-44.07,2,1.72,3.86,3.13,5.55,4.41a73.6,73.6,0,0,1,6.82,5.67,12.51,12.51,0,0,0,8.64,3.47h67.89a12.5,12.5,0,0,0,0-25ZM137.54,394a7.11,7.11,0,0,0,.57-1.22A7.21,7.21,0,0,1,137.54,394Zm-4.2-16.49c-3.9-8.87-9.21-21.57-11.16-29.13-12.3-47.78-14.92-84.49-15-106.74a176.24,176.24,0,0,0,36.32,45.81l-.44,58.72a3.4,3.4,0,0,0,3.46,3.38h6.71C151,355.14,144.29,364.51,133.34,377.52ZM222,396.46c-1.95,3.24-9.87,12.67-15.22,19-4.19,5-7.28,8.68-9.55,11.52-10.77-11.57-21.45-24.77-23.55-31.2-2.91-8.91-4.42-26.92-5.14-46.25h5.54a3.47,3.47,0,0,0,3.52-3.38l.38-34.42a177,177,0,0,0,33.82,14l-.28,21.88a2.72,2.72,0,0,0,.86,1.18,4,4,0,0,0,2.62.75h6C221.76,367,222.43,386.89,222,396.46Zm65.51-.85a18,18,0,0,1-3.24,10.33c-6.32,8.91-15.92,22.51-22,31.12-6.28-8.85-16.34-23.05-22.3-31.71a16.65,16.65,0,0,1-2.88-9.39v-46.4h5.63c1.95,0,3.53-.86,3.55-1.93l.28-15.29q7.62.68,15.38.68a176.26,176.26,0,0,0,25.58-1.87Zm62.46.2c-2.19,6.72-13.74,20.6-23.55,31.19-2.27-2.84-5.36-6.53-9.54-11.51-5.35-6.36-13.28-15.79-15.23-19-.09-1.91-.13-4.24-.14-6.87V387.1c.07-17,1.41-44.21,2.22-59.1a177,177,0,0,0,41.09-15.67l.2,33.73a3.49,3.49,0,0,0,3.56,3.38h6.54C354.37,368.82,352.86,386.88,349.94,395.81Zm17.19-116.76a169.09,169.09,0,0,1-198.57,9.27,161.39,161.39,0,0,1-58.83-77.39,152.23,152.23,0,0,1,304.33,0A161.23,161.23,0,0,1,367.13,279.05Zm18.36,113.74a7.11,7.11,0,0,0,.57,1.22A5.94,5.94,0,0,1,385.49,392.79Zm15.92-44.4c-1.94,7.56-7.25,20.26-11.15,29.12-13.21-15.69-18.24-23.84-20-28.07h6a3.44,3.44,0,0,0,3.5-3.38l-1-57.29A176.27,176.27,0,0,0,416.42,242C416.35,264.19,413.69,300.71,401.41,348.39Z"/></g></g></g></svg>
\ No newline at end of file @@ -17,6 +17,8 @@ midimonster: LDLIBS = -ldl # Replace version string with current git-describe if possible ifneq "$(GITVERSION)" "" midimonster: CFLAGS += -DMIDIMONSTER_VERSION=\"$(GITVERSION)\" +midimonster.exe: CFLAGS += -DMIDIMONSTER_VERSION=\"$(GITVERSION)\" +resource.o: RCCFLAGS += -DMIDIMONSTER_VERSION=\\\"$(GITVERSION)\\\" endif # Work around strange linker passing convention differences in Linux and OSX @@ -53,17 +55,25 @@ backends-full: midimonster: midimonster.c portability.h $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) $(LDLIBS) -o $@ +resource.o: midimonster.rc midimonster.ico + $(RCC) $(RCCFLAGS) $< -o $@ --output-format=coff + +midimonster.ico: MIDIMonster.svg + convert -density 384 $< -define icon:auto-resize $@ + midimonster.exe: export CC = x86_64-w64-mingw32-gcc +midimonster.exe: RCC ?= x86_64-w64-mingw32-windres 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 $@ +midimonster.exe: midimonster.c portability.h $(OBJS) resource.o + $(CC) $(CFLAGS) $(LDFLAGS) $< $(OBJS) resource.o $(LDLIBS) -o $@ clean: $(RM) midimonster $(RM) midimonster.exe $(RM) libmmapi.a + $(RM) resource.o $(RM) $(OBJS) $(MAKE) -C backends clean @@ -78,8 +88,11 @@ install: install -d "$(DESTDIR)$(EXAMPLES)" install -m 0644 configs/* "$(DESTDIR)$(EXAMPLES)" ifdef DEFAULT_CFG +# Only install the default configuration if it is not already present to avoid overwriting it +ifeq (,$(wildcard $(DEFAULT_CFG))) install -Dm 0644 monster.cfg "$(DESTDIR)$(DEFAULT_CFG)" endif +endif sanitize: export CC = clang sanitize: export CFLAGS += -g -Wall -Wpedantic -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer @@ -1,46 +1,60 @@ # The MIDIMonster +<img align="right" src="/MIDIMonster.svg?raw=true&sanitize=true" alt="MIDIMonster Logo" width="20%"> -Named for its scary math, the MIDIMonster is a universal translation -tool between multi-channel absolute-value-based control and/or bus protocols. +[![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) +[![IRC Channel](https://static.midimonster.net/hackint-badge.svg)](https://webirc.hackint.org/#irc://irc.hackint.org/#midimonster) + +Named for its scary math, the MIDIMonster is a universal control and translation +tool for multi-channel absolute-value-based control and/or bus protocols. Currently, the MIDIMonster supports the following protocols: -| Protocol | Operating Systems | Notes | Backends | -|-------------------------------|-----------------------|-------------------------------|-------------------------------| +| Protocol / Interface | 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: - -* Translate MIDI Control Changes into Notes ([Example configuration](configs/unifest-17.cfg)) +| 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) | +| OpenPixelControl | Linux, Windows, OSX | 8 Bit & 16 Bit modes | [`openpixelcontrol`](backends/openpixelcontrol.md) | +| evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | +| Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | +| MA Lighting Web Remote | Linux, Windows, OSX | GrandMA2 and dot2 (incl. OnPC) | [`maweb`](backends/maweb.md) | +| JACK/LV2 Control Voltage (CV) | Linux, OSX | | [`jack`](backends/jack.md) | +| Lua Scripting | Linux, Windows, OSX | | [`lua`](backends/lua.md) | +| Python Scripting | Linux, OSX | | [`python`](backends/python.md) | +| Loopback | Linux, Windows, OSX | | [`loopback`](backends/loopback.md) | + +With these features, the MIDIMonster allows users to control any channel on any of these protocols, and translate any channel on +one protocol into channel(s) on any other (or the same) supported protocol, for example to: + +* Translate MIDI Control Changes into MIDI 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)) +* Dynamically generate, route and modify events using the Lua programming language ([Example configuration](configs/lua.cfg) and [Script](configs/demo.lua)) + to create your own lighting controller or run effects on TouchOSC (Flying faders demo [configuration](configs/flying-faders.cfg) and [script](configs/flying-faders.lua)) * Use an OSC app as a simple lighting controller via ArtNet or sACN * Visualize ArtNet data using OSC tools * Control lighting fixtures or DAWs using gamepad controllers, trackballs, etc ([Example configuration](configs/evdev.cfg)) * Play games, type, or control your mouse using MIDI controllers ([Example configuration](configs/midi-mouse.cfg)) -[![Build Status](https://travis-ci.com/cbdevnet/midimonster.svg?branch=master)](https://travis-ci.com/cbdevnet/midimonster) [![Coverity Scan Build Status](https://scan.coverity.com/projects/15168/badge.svg)](https://scan.coverity.com/projects/15168) +If you encounter a bug or suspect a problem with a protocol implementation, please +[open an Issue](https://github.com/cbdevnet/midimonster/issues) or get in touch with us via +IRC on [Hackint in `#midimonster`](https://webirc.hackint.org/#irc://irc.hackint.org/#midimonster). +We are happy to hear from you! # Table of Contents - * [Usage](#usage) - * [Configuration](#configuration) - * [Backend documentation](#backend-documentation) - * [Building](#building) - + [Prerequisites](#prerequisites) - + [Build](#build) - * [Development](#development) +* [Usage](#usage) +* [Configuration](#configuration) +* [Backend documentation](#backend-documentation) +* [Installation](#installation) + + [Using the installer](#using-the-installer) + + [Building from source](#building-from-source) + - [Building for Linux/OSX](#building-for-linuxosx) + - [Building for Packaging](#building-for-packaging) + - [Building for Windows](#building-for-windows) +* [Development](#development) ## Usage @@ -48,6 +62,8 @@ The MIDImonster takes as it's first argument the name of an optional configurati to use (`monster.cfg` is used as default if none is specified). The configuration file syntax is explained in the next section. +The current MIDIMonster version can be queried by passing *-v* as command-line argument. + ## Configuration Each protocol supported by MIDIMonster is implemented by a *backend*, which takes @@ -76,10 +92,14 @@ 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. +Backend and instance configuration options can also be overridden via command line +arguments using the syntax `-b <backend>.<option>=<value>` for backend options +and `-i <instance>.<option>=<value>` for instance options. These overrides +are applied when the backend/instance is first mentioned in the configuration file. + ### Channel mapping The `[map]` section consists of lines of channel-to-channel assignments, reading like - ``` instance.channel-a < instance.channel-b instance.channel-a > instance.channel-b @@ -98,7 +118,7 @@ The last line is a shorter way to create a bi-directional mapping. 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 +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. @@ -106,7 +126,6 @@ Both sides of a multi-channel assignment need to have the same number of channel side must have exactly one channel. Example multi-channel mapping: - ``` instance-a.channel{1..10} > instance-b.{10..1} ``` @@ -126,36 +145,89 @@ special information. These documentation files are located in the `backends/` di * [`loopback` backend documentation](backends/loopback.md) * [`ola` backend documentation](backends/ola.md) * [`osc` backend documentation](backends/osc.md) +* [`openpixelcontrol` backend documentation](backends/openpixelcontrol.md) * [`lua` backend documentation](backends/lua.md) +* [`python` backend documentation](backends/python.md) * [`maweb` backend documentation](backends/maweb.md) -## Building +## Installation -This section will explain how to build the provided sources to be able to run -`midimonster`. +This section will explain how to build and install the MIDIMonster. +Development is mainly done on Linux, but builds for OSX and Windows +are possible. -### Prerequisites +Binary builds for all supported systems are available for download on the +[Release page](https://github.com/cbdevnet/midimonster/releases). -In order to build the MIDIMonster, you'll need some libraries that provide -support for the protocols to translate. +### Using the installer + +The easiest way to install MIDIMonster and its dependencies on a Linux system +is the [installer script](installer.sh). + +The following commands download the installer, make it executable and finally, start it: + +``` +wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh ./ +chmod +x ./installer.sh +./installer.sh +``` + +The installer can also be used for automating installations or upgrades by specifying additional +command line arguments. To see a list of valid arguments, run the installer with the +`--help` argument. + +The installer script can also update MIDIMonster to the latest version automatically, +using a configuration file generated during the installation. +To do so, run `midimonster-updater` as root on your system after using the installer. + +If you prefer to install a Debian package you can download the `.deb` file from our +[Release page](https://github.com/cbdevnet/midimonster/releases). +To install the package, run the following command as the root user: + +``` +dpkg -i <file>.deb +``` + +### Building from source + +To build the MIDIMonster directly from the sources, you'll need some libraries that provide +support for the protocols to translate. When building from source, you can also choose to +exclude backends (for example, if you don't need them or don't want to install their +prerequisites). * `libasound2-dev` (for the ALSA MIDI backend) * `libevdev-dev` (for the evdev backend) * `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) +* `python3-dev` (for the Python backend) +* `pkg-config` (as some projects and systems like to spread their files around) * A C compiler * GNUmake To build for Windows, the package `mingw-w64` provides a cross-compiler that can be used to build a subset of the backends as well as the core. -### Build +#### Building for Linux/OSX For Linux and OSX, just running `make` in the source directory should do the trick. +Some backends have been marked as optional as they require rather large additional software to be installed, +for example the `ola` backend. To create a build including these, run `make full`. + +To install a source build with `make install`, please familiarize yourself with the build parameters +as specified in the next section. + +Backends may also be built selectively by running `make <backendfile>` in the `backends/` directory, +for example + +``` +make jack.so +``` + +#### Building for Packaging + The build process accepts the following parameters, either from the environment or as arguments to the `make` invocation: @@ -173,29 +245,6 @@ Note that the same variables may have different default values depending on the builds that are destined to be installed require those variables to be set to the same value for the build and `install` targets. -Some backends have been marked as optional as they require rather large additional software to be installed, -for example the `ola` backend. To create a build including these, run `make full`. - -Backends may also be built selectively by running `make <backendfile>` in the `backends/` directory, -for example - -``` -make jack.so -``` -#### Using the installer - -For easy installation on Linux, the [installer script](installer.sh) can be used: - -``` -wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh ./ -chmod +x ./installer.sh -./installer.sh -``` -This tool can also update MIDImonster automatically using a configuration file generated by the installer. -To do so, run `midimonster-updater` as root on your system after using the installer. - -#### Building for packaging or installation - For system-wide install or packaging builds, the following steps are recommended: ``` @@ -5,3 +5,5 @@ udp backends may ignore MTU mm_managed_fd.impl is not freed currently (and is heaped most of the time anyway) -> documentation make event collectors threadsafe to stop marshalling data... collect & check backend API version +windows strerror +move all connection establishment to _start to be able to hot-stop/start all backends @@ -4,26 +4,44 @@ #else #define MM_API __attribute__((dllexport)) #endif +#define BACKEND_NAME "core/be" #include "midimonster.h" #include "backend.h" -static size_t nbackends = 0; -static backend* backends = NULL; -static size_t ninstances = 0; -static instance** instances = NULL; -static size_t nchannels = 0; -static channel** channels = NULL; +static struct { + size_t n; + backend* backends; + instance*** instances; +} registry = { + .n = 0 +}; + +//the global channel store was converted from a naive list to a hashmap of lists for performance reasons +static struct { + //channelstore hash is set up for 256 buckets + size_t n[256]; + channel** entry[256]; +} channels = { + .n = { + 0 + } +}; + +static size_t channelstore_hash(instance* inst, uint64_t ident){ + uint64_t repr = ((uint64_t) inst) ^ ident; + return (repr ^ (repr >> 8) ^ (repr >> 16) ^ (repr >> 24) ^ (repr >> 32)) & 0xFF; +} int backends_handle(size_t nfds, managed_fd* fds){ size_t u, p, n; int rv = 0; managed_fd xchg; - for(u = 0; u < nbackends && !rv; u++){ + for(u = 0; u < registry.n && !rv; u++){ n = 0; for(p = 0; p < nfds; p++){ - if(fds[p].backend == backends + u){ + if(fds[p].backend == registry.backends + u){ xchg = fds[n]; fds[n] = fds[p]; fds[p] = xchg; @@ -31,10 +49,13 @@ int backends_handle(size_t nfds, managed_fd* fds){ } } - DBGPF("Notifying backend %s of %lu waiting FDs\n", backends[u].name, n); - rv |= backends[u].process(n, fds); - if(rv){ - fprintf(stderr, "Backend %s failed to handle input\n", backends[u].name); + //handle if there is data ready or the backend has active instances for polling + if(n || registry.instances[u]){ + DBGPF("Notifying backend %s of %" PRIsize_t " waiting FDs\n", registry.backends[u].name, n); + rv |= registry.backends[u].process(n, fds); + if(rv){ + fprintf(stderr, "Backend %s failed to handle input\n", registry.backends[u].name); + } } } return rv; @@ -44,166 +65,149 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ size_t u, p, n; int rv = 0; channel_value xval; - channel* xchnl; + channel* xchnl = NULL; - //TODO eliminate duplicates - for(u = 0; u < ninstances && !rv; u++){ - n = 0; + for(u = 0; u < nev && !rv; u = n){ + //sort for this instance + n = u + 1; + for(p = u + 1; p < nev; p++){ + if(c[p]->instance == c[u]->instance){ + xval = v[p]; + xchnl = c[p]; - for(p = 0; p < nev; p++){ - if(c[p]->instance == instances[u]){ - xval = v[n]; - xchnl = c[n]; - - v[n] = v[p]; - c[n] = c[p]; + v[p] = v[n]; + c[p] = c[n]; - v[p] = xval; - c[p] = xchnl; + v[n] = xval; + c[n] = xchnl; n++; } } - DBGPF("Calling handler for instance %s with %lu events\n", instances[u]->name, n); - rv |= instances[u]->backend->handle(instances[u], n, c, v); + //TODO eliminate duplicates + DBGPF("Calling handler for instance %s with %" PRIsize_t " events\n", c[u]->instance->name, n - u); + rv |= c[u]->instance->backend->handle(c[u]->instance, n - u, c + u, v + u); } return 0; } MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){ - size_t u; - for(u = 0; u < nchannels; u++){ - if(channels[u]->instance == inst && channels[u]->ident == ident){ - DBGPF("Requested channel %lu on instance %s already exists, reusing\n", ident, inst->name); - return channels[u]; + size_t u, bucket = channelstore_hash(inst, ident); + for(u = 0; u < channels.n[bucket]; u++){ + if(channels.entry[bucket][u]->instance == inst + && channels.entry[bucket][u]->ident == ident){ + DBGPF("Requested channel %" PRIu64 " on instance %s already exists, reusing (%" PRIsize_t " search steps)\n", ident, inst->name, u); + return channels.entry[bucket][u]; } } if(!create){ - DBGPF("Requested unknown channel %lu on instance %s\n", ident, inst->name); + DBGPF("Requested unknown channel %" PRIu64 " on instance %s\n", ident, inst->name); return NULL; } - DBGPF("Creating previously unknown channel %lu on instance %s\n", ident, inst->name); - channel** new_chan = realloc(channels, (nchannels + 1) * sizeof(channel*)); - if(!new_chan){ + DBGPF("Creating previously unknown channel %" PRIu64 " on instance %s, bucket %" PRIsize_t "\n", ident, inst->name, bucket); + channels.entry[bucket] = realloc(channels.entry[bucket], (channels.n[bucket] + 1) * sizeof(channel*)); + if(!channels.entry[bucket]){ fprintf(stderr, "Failed to allocate memory\n"); - nchannels = 0; + channels.n[bucket] = 0; return NULL; } - channels = new_chan; - channels[nchannels] = calloc(1, sizeof(channel)); - if(!channels[nchannels]){ + channels.entry[bucket][channels.n[bucket]] = calloc(1, sizeof(channel)); + if(!channels.entry[bucket][channels.n[bucket]]){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; } - channels[nchannels]->instance = inst; - channels[nchannels]->ident = ident; - return channels[nchannels++]; + channels.entry[bucket][channels.n[bucket]]->instance = inst; + channels.entry[bucket][channels.n[bucket]]->ident = ident; + return channels.entry[bucket][(channels.n[bucket]++)]; } -MM_API instance* mm_instance(){ - instance** new_inst = realloc(instances, (ninstances + 1) * sizeof(instance*)); - if(!new_inst){ - //TODO free - fprintf(stderr, "Failed to allocate memory\n"); - ninstances = 0; - return NULL; - } - instances = new_inst; - instances[ninstances] = calloc(1, sizeof(instance)); - if(!instances[ninstances]){ - fprintf(stderr, "Failed to allocate memory\n"); - return NULL; - } +instance* mm_instance(backend* b){ + size_t u = 0, n = 0; - return instances[ninstances++]; -} - -MM_API instance* mm_instance_find(char* name, uint64_t ident){ - size_t u; - backend* b = backend_match(name); - if(!b){ - return NULL; - } + for(u = 0; u < registry.n; u++){ + if(registry.backends + u == b){ + //count existing instances + for(n = 0; registry.instances[u] && registry.instances[u][n]; n++){ + } - for(u = 0; u < ninstances; u++){ - if(instances[u]->backend == b && instances[u]->ident == ident){ - return instances[u]; + //extend + registry.instances[u] = realloc(registry.instances[u], (n + 2) * sizeof(instance*)); + if(!registry.instances[u]){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + //sentinel + registry.instances[u][n + 1] = NULL; + registry.instances[u][n] = calloc(1, sizeof(instance)); + if(!registry.instances[u][n]){ + fprintf(stderr, "Failed to allocate memory\n"); + } + registry.instances[u][n]->backend = b; + return registry.instances[u][n]; } } + //this should never happen return NULL; } -MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ - backend* b = backend_match(name); - size_t n = 0, u; - //count number of affected instances - for(u = 0; u < ninstances; u++){ - if(instances[u]->backend == b){ - n++; +MM_API instance* mm_instance_find(char* name, uint64_t ident){ + size_t b = 0; + instance** iter = NULL; + for(b = 0; b < registry.n; b++){ + if(!strcmp(registry.backends[b].name, name)){ + for(iter = registry.instances[b]; iter && *iter; iter++){ + if((*iter)->ident == ident){ + return *iter; + } + } } } - *ninst = n; - - if(!n){ - *inst = NULL; - return 0; - } + return NULL; +} - *inst = calloc(n, sizeof(instance*)); - if(!*inst){ - fprintf(stderr, "Failed to allocate memory\n"); +MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ + size_t b = 0, i = 0; + if(!ninst || !inst){ return 1; } - n = 0; - for(u = 0; u < ninstances; u++){ - if(instances[u]->backend == b){ - (*inst)[n] = instances[u]; - n++; - } - } - return 0; -} + for(b = 0; b < registry.n; b++){ + if(!strcmp(registry.backends[b].name, name)){ + //count instances + for(i = 0; registry.instances[b] && registry.instances[b][i]; i++){ + } -void instances_free(){ - size_t u; - for(u = 0; u < ninstances; u++){ - free(instances[u]->name); - instances[u]->name = NULL; - instances[u]->backend = NULL; - free(instances[u]); - instances[u] = NULL; - } - free(instances); - ninstances = 0; -} + *ninst = i; + if(!i){ + *inst = NULL; + return 0; + } -void channels_free(){ - size_t u; - for(u = 0; u < nchannels; u++){ - DBGPF("Destroying channel %lu on instance %s\n", channels[u]->ident, channels[u]->instance->name); - if(channels[u]->impl){ - channels[u]->instance->backend->channel_free(channels[u]); + *inst = calloc(i, sizeof(instance*)); + if(!*inst){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + memcpy(*inst, registry.instances[b], i * sizeof(instance*)); + return 0; } - free(channels[u]); - channels[u] = NULL; } - free(channels); - nchannels = 0; + return 1; } backend* backend_match(char* name){ size_t u; - for(u = 0; u < nbackends; u++){ - if(!strcmp(backends[u].name, name)){ - return backends + u; + for(u = 0; u < registry.n; u++){ + if(!strcmp(registry.backends[u].name, name)){ + return registry.backends + u; } } return NULL; @@ -211,9 +215,12 @@ backend* backend_match(char* name){ instance* instance_match(char* name){ size_t u; - for(u = 0; u < ninstances; u++){ - if(!strcmp(instances[u]->name, name)){ - return instances[u]; + instance** iter = NULL; + for(u = 0; u < registry.n; u++){ + for(iter = registry.instances[u]; iter && *iter; iter++){ + if(!strcmp(name, (*iter)->name)){ + return *iter; + } } } return NULL; @@ -223,14 +230,17 @@ struct timeval backend_timeout(){ size_t u; uint32_t res, secs = 1, msecs = 0; - for(u = 0; u < nbackends; u++){ - if(backends[u].interval){ - res = backends[u].interval(); + for(u = 0; u < registry.n; u++){ + //only call interval if backend has instances + if(registry.instances[u] && registry.backends[u].interval){ + res = registry.backends[u].interval(); if((res / 1000) < secs){ + DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); secs = res / 1000; msecs = res % 1000; } else if(res / 1000 == secs && (res % 1000) < msecs){ + DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); msecs = res % 1000; } } @@ -245,14 +255,16 @@ struct timeval backend_timeout(){ MM_API int mm_backend_register(backend b){ if(!backend_match(b.name)){ - backends = realloc(backends, (nbackends + 1) * sizeof(backend)); - if(!backends){ + registry.backends = realloc(registry.backends, (registry.n + 1) * sizeof(backend)); + registry.instances = realloc(registry.instances, (registry.n + 1) * sizeof(instance**)); + if(!registry.backends || !registry.instances){ fprintf(stderr, "Failed to allocate memory\n"); - nbackends = 0; + registry.n = 0; return 1; } - backends[nbackends] = b; - nbackends++; + registry.backends[registry.n] = b; + registry.instances[registry.n] = NULL; + registry.n++; fprintf(stderr, "Registered backend %s\n", b.name); return 0; @@ -262,29 +274,25 @@ MM_API int mm_backend_register(backend b){ int backends_start(){ int rv = 0, current; - size_t n, u, p; instance** inst = NULL; + size_t n, u; - for(u = 0; u < nbackends; u++){ - //only start backends that have instances - for(p = 0; p < ninstances && instances[p]->backend != backends + u; p++){ - } - - //backend has no instances, skip the start call - if(p == ninstances){ + for(u = 0; u < registry.n; u++){ + //skip backends without instances + if(!registry.instances[u]){ continue; } - + //fetch list of instances - if(mm_backend_instances(backends[u].name, &n, &inst)){ - fprintf(stderr, "Failed to fetch instance list for initialization of backend %s\n", backends[u].name); + if(mm_backend_instances(registry.backends[u].name, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list for initialization of backend %s\n", registry.backends[u].name); return 1; } //start the backend - current = backends[u].start(n, inst); + current = registry.backends[u].start(n, inst); if(current){ - fprintf(stderr, "Failed to start backend %s\n", backends[u].name); + fprintf(stderr, "Failed to start backend %s\n", registry.backends[u].name); } //clean up @@ -295,24 +303,57 @@ int backends_start(){ return rv; } +static void channels_free(){ + size_t u, p; + for(u = 0; u < sizeof(channels.n) / sizeof(channels.n[0]); u++){ + DBGPF("Cleaning up channel registry bucket %" PRIsize_t " with %" PRIsize_t " channels", u, channels.n[u]); + for(p = 0; p < channels.n[u]; p++){ + DBGPF("Destroying channel %" PRIu64 " on instance %s\n", channels.entry[u][p]->ident, channels.entry[u][p]->instance->name); + //call the channel_free function if the backend supports it + if(channels.entry[u][p]->impl && channels.entry[u][p]->instance->backend->channel_free){ + channels.entry[u][p]->instance->backend->channel_free(channels.entry[u][p]); + } + free(channels.entry[u][p]); + } + free(channels.entry[u]); + channels.entry[u] = NULL; + channels.n[u] = 0; + } +} + int backends_stop(){ size_t u, n; instance** inst = NULL; - for(u = 0; u < nbackends; u++){ + //channels before instances to support proper shutdown procedures + channels_free(); + + //shut down the registry + for(u = 0; u < registry.n; u++){ //fetch list of instances - if(mm_backend_instances(backends[u].name, &n, &inst)){ - fprintf(stderr, "Failed to fetch instance list for shutdown of backend %s\n", backends[u].name); - n = 0; + if(mm_backend_instances(registry.backends[u].name, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list for shutdown of backend %s\n", registry.backends[u].name); inst = NULL; + n = 0; } - backends[u].shutdown(n, inst); + registry.backends[u].shutdown(n, inst); free(inst); inst = NULL; + + //free instances + for(inst = registry.instances[u]; inst && *inst; inst++){ + free((*inst)->name); + (*inst)->name = NULL; + (*inst)->backend = NULL; + free(*inst); + } + free(registry.instances[u]); + registry.instances[u] = NULL; } - free(backends); - nbackends = 0; + free(registry.backends); + free(registry.instances); + registry.n = 0; return 0; } @@ -8,12 +8,10 @@ instance* instance_match(char* name); struct timeval backend_timeout(); int backends_start(); int backends_stop(); -void instances_free(); -void channels_free(); +instance* mm_instance(backend* b); /* 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 f0d5c3e..8956a20 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.so -WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll rtpmidi.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so rtpmidi.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.dll OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o @@ -36,6 +36,10 @@ sacn.so: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) sacn.dll: LDLIBS += -lws2_32 +openpixelcontrol.so: ADDITIONAL_OBJS += $(BACKEND_LIB) +openpixelcontrol.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) +openpixelcontrol.dll: LDLIBS += -lws2_32 + maweb.so: ADDITIONAL_OBJS += $(BACKEND_LIB) maweb.so: LDLIBS = -lssl maweb.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) @@ -63,6 +67,9 @@ lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") lua.dll: LDLIBS += -L../libs -llua53 +python.so: CFLAGS += $(shell pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") +python.so: CFLAGS += $(shell pkg-config --libs python3 || pkg-config --libs python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") + %.so :: %.c %.h $(BACKEND_LIB) $(CC) $(CFLAGS) $(LDLIBS) $< $(ADDITIONAL_OBJS) -o $@ $(LDFLAGS) diff --git a/backends/artnet.c b/backends/artnet.c index 0bd1a32..caab6e0 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -9,6 +9,7 @@ #define MAX_FDS 255 +static uint32_t next_frame = 0; static uint8_t default_net = 0; static size_t artnet_fds = 0; static artnet_descriptor* artnet_fd = NULL; @@ -37,7 +38,6 @@ static int artnet_listener(char* host, char* port){ artnet_fd[artnet_fds].fd = fd; artnet_fd[artnet_fds].output_instances = 0; artnet_fd[artnet_fds].output_instance = NULL; - artnet_fd[artnet_fds].last_frame = NULL; artnet_fds++; return 0; } @@ -52,6 +52,7 @@ MM_PLUGIN_API int init(){ .handle = artnet_set, .process = artnet_handle, .start = artnet_start, + .interval = artnet_interval, .shutdown = artnet_shutdown }; @@ -68,6 +69,13 @@ MM_PLUGIN_API int init(){ return 0; } +static uint32_t artnet_interval(){ + if(next_frame){ + return next_frame; + } + return ARTNET_KEEPALIVE_INTERVAL; +} + static int artnet_configure(char* option, char* value){ char* host = NULL, *port = NULL, *fd_opts = NULL; if(!strcmp(option, "net")){ @@ -94,23 +102,23 @@ static int artnet_configure(char* option, char* value){ return 1; } -static instance* artnet_instance(){ - artnet_instance_data* data = NULL; - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } +static int artnet_instance(instance* inst){ + artnet_instance_data* data = calloc(1, sizeof(artnet_instance_data)); + size_t u; - data = calloc(1, sizeof(artnet_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->net = default_net; + for(u = 0; u < sizeof(data->data.channel) / sizeof(channel); u++){ + data->data.channel[u].ident = u; + data->data.channel[u].instance = inst; + } inst->impl = data; - return inst; + return 0; } static int artnet_configure_instance(instance* inst, char* option, char* value){ @@ -162,6 +170,11 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ } chan_a--; + //check output capabilities + if((flags & mmchannel_output) && !data->dest_len){ + LOGPF("Channel %s.%s mapped for output, but instance is not configured for output (missing destination)", inst->name, spec); + } + //secondary channel setup if(*spec_next == '+'){ chan_b = strtoul(spec_next + 1, NULL, 10); @@ -190,7 +203,7 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ } data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a); - return mm_channel(inst, chan_a, 1); + return data->data.channel + chan_a; } static int artnet_transmit(instance* inst){ @@ -217,14 +230,16 @@ static int artnet_transmit(instance* inst){ //update last frame timestamp for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){ if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){ - artnet_fd[data->fd_index].last_frame[u] = mm_timestamp(); + artnet_fd[data->fd_index].output_instance[u].last_frame = mm_timestamp(); + artnet_fd[data->fd_index].output_instance[u].mark = 0; } } return 0; } static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v){ - size_t u, mark = 0; + uint32_t frame_delta = 0; + size_t u, mark = 0, channel_offset = 0; artnet_instance_data* data = (artnet_instance_data*) inst->impl; if(!data->dest_len){ @@ -232,28 +247,44 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) return 0; } - //FIXME maybe introduce minimum frame interval for(u = 0; u < num; u++){ - if(IS_WIDE(data->data.map[c[u]->ident])){ + channel_offset = c[u]->ident; + if(IS_WIDE(data->data.map[channel_offset])){ uint32_t val = v[u].normalised * ((double) 0xFFFF); //the primary (coarse) channel is the one registered to the core, so we don't have to check for that - if(data->data.out[c[u]->ident] != ((val >> 8) & 0xFF)){ + if(data->data.out[channel_offset] != ((val >> 8) & 0xFF)){ mark = 1; - data->data.out[c[u]->ident] = (val >> 8) & 0xFF; + data->data.out[channel_offset] = (val >> 8) & 0xFF; } - if(data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){ + if(data->data.out[MAPPED_CHANNEL(data->data.map[channel_offset])] != (val & 0xFF)){ mark = 1; - data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF; + data->data.out[MAPPED_CHANNEL(data->data.map[channel_offset])] = val & 0xFF; } } - else if(data->data.out[c[u]->ident] != (v[u].normalised * 255.0)){ + else if(data->data.out[channel_offset] != (v[u].normalised * 255.0)){ mark = 1; - data->data.out[c[u]->ident] = v[u].normalised * 255.0; + data->data.out[channel_offset] = v[u].normalised * 255.0; } } if(mark){ + //find last frame time + for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){ + if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){ + break; + } + } + + frame_delta = mm_timestamp() - artnet_fd[data->fd_index].output_instance[u].last_frame; + //check output rate limit, request next frame + if(frame_delta < ARTNET_FRAME_TIMEOUT){ + artnet_fd[data->fd_index].output_instance[u].mark = 1; + if(!next_frame || next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){ + next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta); + } + return 0; + } return artnet_transmit(inst); } @@ -285,16 +316,9 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ for(p = 0; p <= max_mark; p++){ if(data->data.map[p] & MAP_MARK){ data->data.map[p] &= ~MAP_MARK; + chan = data->data.channel + p; if(data->data.map[p] & MAP_FINE){ - chan = mm_channel(inst, MAPPED_CHANNEL(data->data.map[p]), 0); - } - else{ - chan = mm_channel(inst, p, 0); - } - - if(!chan){ - LOGPF("Active channel %" PRIsize_t " on %s not known to core", p, inst->name); - return 1; + chan = data->data.channel + MAPPED_CHANNEL(data->data.map[p]); } if(IS_WIDE(data->data.map[p])){ @@ -323,6 +347,7 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ static int artnet_handle(size_t num, managed_fd* fds){ size_t u, c; uint64_t timestamp = mm_timestamp(); + uint32_t synthesize_delta = 0; ssize_t bytes_read; char recv_buf[ARTNET_RECV_BUF]; artnet_instance_id inst_id = { @@ -331,15 +356,25 @@ static int artnet_handle(size_t num, managed_fd* fds){ instance* inst = NULL; artnet_pkt* frame = (artnet_pkt*) recv_buf; - //transmit keepalive frames + //transmit keepalive & synthesized frames + next_frame = 0; for(u = 0; u < artnet_fds; u++){ for(c = 0; c < artnet_fd[u].output_instances; c++){ - if(timestamp - artnet_fd[u].last_frame[c] >= ARTNET_KEEPALIVE_INTERVAL){ + synthesize_delta = timestamp - artnet_fd[u].output_instance[c].last_frame; + if((artnet_fd[u].output_instance[c].mark + && synthesize_delta >= ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN) //synthesize next frame + || synthesize_delta >= ARTNET_KEEPALIVE_INTERVAL){ //keepalive timeout inst = mm_instance_find(BACKEND_NAME, artnet_fd[u].output_instance[c].label); if(inst){ artnet_transmit(inst); } } + + //update next_frame + if(artnet_fd[u].output_instance[c].mark + && (!next_frame || next_frame > ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta)){ + next_frame = ARTNET_FRAME_TIMEOUT + ARTNET_SYNTHESIZE_MARGIN - synthesize_delta; + } } } @@ -413,15 +448,15 @@ static int artnet_start(size_t n, instance** inst){ //if enabled for output, add to keepalive tracking if(data->dest_len){ - artnet_fd[data->fd_index].output_instance = realloc(artnet_fd[data->fd_index].output_instance, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(artnet_instance_id)); - artnet_fd[data->fd_index].last_frame = realloc(artnet_fd[data->fd_index].last_frame, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(uint64_t)); + artnet_fd[data->fd_index].output_instance = realloc(artnet_fd[data->fd_index].output_instance, (artnet_fd[data->fd_index].output_instances + 1) * sizeof(artnet_output_universe)); - if(!artnet_fd[data->fd_index].output_instance || !artnet_fd[data->fd_index].last_frame){ + if(!artnet_fd[data->fd_index].output_instance){ LOG("Failed to allocate memory"); goto bail; } - artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances] = id; - artnet_fd[data->fd_index].last_frame[artnet_fd[data->fd_index].output_instances] = 0; + artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].label = id.label; + artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].last_frame = 0; + artnet_fd[data->fd_index].output_instance[artnet_fd[data->fd_index].output_instances].mark = 0; artnet_fd[data->fd_index].output_instances++; } @@ -449,7 +484,6 @@ static int artnet_shutdown(size_t n, instance** inst){ for(p = 0; p < artnet_fds; p++){ close(artnet_fd[p].fd); free(artnet_fd[p].output_instance); - free(artnet_fd[p].last_frame); } free(artnet_fd); diff --git a/backends/artnet.h b/backends/artnet.h index 59bd53f..a517aa0 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -4,9 +4,10 @@ #include "midimonster.h" MM_PLUGIN_API int init(); +static uint32_t artnet_interval(); static int artnet_configure(char* option, char* value); static int artnet_configure_instance(instance* instance, char* option, char* value); -static instance* artnet_instance(); +static int artnet_instance(instance* inst); 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); @@ -16,7 +17,11 @@ static int artnet_shutdown(size_t n, instance** inst); #define ARTNET_PORT "6454" #define ARTNET_VERSION 14 #define ARTNET_RECV_BUF 4096 -#define ARTNET_KEEPALIVE_INTERVAL 2000 + +#define ARTNET_KEEPALIVE_INTERVAL 1000 +//limit transmit rate to at most 44 packets per second (1000/44 ~= 22) +#define ARTNET_FRAME_TIMEOUT 20 +#define ARTNET_SYNTHESIZE_MARGIN 10 #define MAP_COARSE 0x0200 #define MAP_FINE 0x0400 @@ -32,6 +37,7 @@ typedef struct /*_artnet_universe_model*/ { uint8_t in[512]; uint8_t out[512]; uint16_t map[512]; + channel channel[512]; } artnet_universe; typedef struct /*_artnet_instance_model*/ { @@ -52,11 +58,16 @@ typedef union /*_artnet_instance_id*/ { uint64_t label; } artnet_instance_id; +typedef struct /*_artnet_fd_universe*/ { + uint64_t label; + uint64_t last_frame; + uint8_t mark; +} artnet_output_universe; + typedef struct /*_artnet_fd*/ { int fd; size_t output_instances; - artnet_instance_id* output_instance; - uint64_t* last_frame; + artnet_output_universe* output_instance; } artnet_descriptor; #pragma pack(push, 1) diff --git a/backends/artnet.md b/backends/artnet.md index 90a7697..383203d 100644 --- a/backends/artnet.md +++ b/backends/artnet.md @@ -3,6 +3,8 @@ The ArtNet backend provides read-write access to the UDP-based ArtNet protocol for lighting fixture control. +Art-Net™ Designed by and Copyright Artistic Licence Holdings Ltd. + #### Global configuration | Option | Example value | Default value | Description | @@ -36,6 +38,3 @@ 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 4725ef7..af5ec74 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -63,16 +63,11 @@ static int evdev_configure(char* option, char* value) { return 1; } -static instance* evdev_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int evdev_instance(instance* inst){ evdev_instance_data* data = calloc(1, sizeof(evdev_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->input_fd = -1; @@ -81,12 +76,12 @@ static instance* evdev_instance(){ if(!data->output_proto){ LOG("Failed to initialize libevdev output prototype device"); free(data); - return NULL; + return 1; } #endif inst->impl = data; - return inst; + return 0; } static int evdev_attach(instance* inst, evdev_instance_data* data, char* node){ diff --git a/backends/evdev.h b/backends/evdev.h index 0c877fc..e896d2d 100644 --- a/backends/evdev.h +++ b/backends/evdev.h @@ -11,7 +11,7 @@ MM_PLUGIN_API int init(); static int evdev_configure(char* option, char* value); static int evdev_configure_instance(instance* instance, char* option, char* value); -static instance* evdev_instance(); +static int evdev_instance(instance* inst); 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); diff --git a/backends/jack.c b/backends/jack.c index d7f68c4..c862096 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -334,19 +334,13 @@ static int mmjack_configure_instance(instance* inst, char* option, char* value){ return 0; } -static instance* mmjack_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int mmjack_instance(instance* inst){ inst->impl = calloc(1, sizeof(mmjack_instance_data)); if(!inst->impl){ LOG("Failed to allocate memory"); - return NULL; + return 1; } - - return inst; + return 0; } static int mmjack_parse_midispec(mmjack_channel_ident* ident, char* spec){ diff --git a/backends/jack.h b/backends/jack.h index 66c66db..03ce052 100644 --- a/backends/jack.h +++ b/backends/jack.h @@ -5,7 +5,7 @@ MM_PLUGIN_API int init(); static int mmjack_configure(char* option, char* value); static int mmjack_configure_instance(instance* inst, char* option, char* value); -static instance* mmjack_instance(); +static int mmjack_instance(instance* inst); 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); diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index ffa403b..b9513ac 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -153,7 +153,11 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin int mmbackend_send(int fd, uint8_t* data, size_t length){ ssize_t total = 0, sent; while(total < length){ + #ifndef LIBMMBACKEND_TCP_TORTURE sent = send(fd, data + total, length - total, 0); + #else + sent = send(fd, data + total, 1, 0); + #endif if(sent < 0){ LOGPF("Failed to send: %s", strerror(errno)); return 1; diff --git a/backends/loopback.c b/backends/loopback.c index 085d1df..4274832 100644 --- a/backends/loopback.c +++ b/backends/loopback.c @@ -34,19 +34,14 @@ static int loopback_configure_instance(instance* inst, char* option, char* value return 0; } -static instance* loopback_instance(){ - instance* i = mm_instance(); - if(!i){ - return NULL; - } - - i->impl = calloc(1, sizeof(loopback_instance_data)); - if(!i->impl){ +static int loopback_instance(instance* inst){ + inst->impl = calloc(1, sizeof(loopback_instance_data)); + if(!inst->impl){ LOG("Failed to allocate memory"); - return NULL; + return 1; } - return i; + return 0; } static channel* loopback_channel(instance* inst, char* spec, uint8_t flags){ @@ -107,6 +102,7 @@ static int loopback_shutdown(size_t n, instance** inst){ } free(data->name); free(inst[u]->impl); + inst[u]->impl = NULL; } LOG("Backend shut down"); diff --git a/backends/loopback.h b/backends/loopback.h index c508d72..cfb2e19 100644 --- a/backends/loopback.h +++ b/backends/loopback.h @@ -3,7 +3,7 @@ MM_PLUGIN_API int init(); static int loopback_configure(char* option, char* value); static int loopback_configure_instance(instance* inst, char* option, char* value); -static instance* loopback_instance(); +static int loopback_instance(instance* inst); 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); diff --git a/backends/lua.c b/backends/lua.c index ee9e03f..7f80cc7 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -9,15 +9,19 @@ #endif #define LUA_REGISTRY_KEY "_midimonster_lua_instance" +#define LUA_REGISTRY_CURRENT_CHANNEL "_midimonster_lua_channel" +#define LUA_REGISTRY_CURRENT_THREAD "_midimonster_lua_thread" static size_t timers = 0; static lua_timer* timer = NULL; uint64_t timer_interval = 0; #ifdef MMBACKEND_LUA_TIMERFD static int timer_fd = -1; -#else -static uint64_t last_timestamp; #endif +static uint64_t last_timestamp = 0; + +static size_t threads = 0; +static lua_thread* thread = NULL; MM_PLUGIN_API int init(){ backend lua = { @@ -63,6 +67,7 @@ static uint32_t lua_interval(){ next_timer = timer[n].interval - timer[n].delta; } } + DBGPF("Next timer fires in %" PRIu32, next_timer); return next_timer; } return 1000; @@ -74,7 +79,7 @@ static int lua_update_timerfd(){ size_t n = 0; #ifdef MMBACKEND_LUA_TIMERFD struct itimerspec timer_config = { - 0 + {0} }; #endif @@ -85,6 +90,13 @@ static int lua_update_timerfd(){ } } + for(n = 0; n < threads; n++){ + if(thread[n].timeout && (!interval || thread[n].timeout < interval)){ + interval = thread[n].timeout; + } + } + DBGPF("Recalculating timers, minimum is %" PRIu64, interval); + //calculate gcd of all timers if any are active if(interval){ for(n = 0; n < timers; n++){ @@ -97,7 +109,8 @@ static int lua_update_timerfd(){ gcd = residual; } //since we round everything, 10 is the lowest interval we get - if(interval == 10){ + if(interval <= 10){ + interval = 10; break; } } @@ -110,22 +123,106 @@ static int lua_update_timerfd(){ } if(interval == timer_interval){ + DBGPF("Keeping interval at %" PRIu64, interval); return 0; } #ifdef MMBACKEND_LUA_TIMERFD - //configure the new interval + //configure the new interval, 0.0 disarms the timer + DBGPF("Reconfiguring timerfd to %" PRIu64 ".%" PRIu64, timer_config.it_interval.tv_sec, timer_config.it_interval.tv_nsec); timerfd_settime(timer_fd, 0, &timer_config, NULL); #endif timer_interval = interval; return 0; } +static void lua_thread_resume(size_t current_thread){ + //push coroutine reference + lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD); + lua_pushnumber(thread[current_thread].thread, current_thread); + lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); + + //call thread main + DBGPF("Resuming thread %" PRIsize_t " on %s", current_thread, thread[current_thread].instance->name); + if(lua_resume(thread[current_thread].thread, NULL, 0) != LUA_YIELD){ + DBGPF("Thread %" PRIsize_t " on %s terminated", current_thread, thread[current_thread].instance->name); + thread[current_thread].timeout = 0; + } + + //remove coroutine reference + lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD); + lua_pushnil(thread[current_thread].thread); + lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); +} + +static int lua_callback_thread(lua_State* interpreter){ + instance* inst = NULL; + size_t u = threads; + if(lua_gettop(interpreter) != 1){ + LOGPF("Thread function called with %d arguments, expected function", lua_gettop(interpreter)); + return 0; + } + + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + + //make space for a new thread + thread = realloc(thread, (threads + 1) * sizeof(lua_thread)); + if(!thread){ + threads = 0; + LOG("Failed to allocate memory"); + return 0; + } + threads++; + + thread[u].thread = lua_newthread(interpreter); + thread[u].instance = inst; + thread[u].timeout = 0; + thread[u].reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + + DBGPF("Registered thread %" PRIsize_t " on %s", threads, inst->name); + + //push thread main + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + lua_pushvalue(interpreter, 1); + lua_xmove(interpreter, thread[u].thread, 1); + + lua_thread_resume(u); + lua_update_timerfd(); + return 0; +} + +static int lua_callback_sleep(lua_State* interpreter){ + uint64_t timeout = 0; + size_t current_thread = threads; + if(lua_gettop(interpreter) != 1){ + LOGPF("Sleep function called with %d arguments, expected number", lua_gettop(interpreter)); + return 0; + } + + timeout = luaL_checkinteger(interpreter, 1); + + lua_pushstring(interpreter, LUA_REGISTRY_CURRENT_THREAD); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + + current_thread = luaL_checkinteger(interpreter, -1); + + if(current_thread < threads){ + DBGPF("Yielding for %" PRIu64 "msec on thread %" PRIsize_t, timeout, current_thread); + thread[current_thread].timeout = timeout; + lua_yield(interpreter, 0); + } + return 0; +} + static int lua_callback_output(lua_State* interpreter){ size_t n = 0; channel_value val; const char* channel_name = NULL; - channel* channel = NULL; instance* inst = NULL; lua_instance_data* data = NULL; @@ -144,15 +241,21 @@ static int lua_callback_output(lua_State* interpreter){ channel_name = lua_tostring(interpreter, 1); val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); + //if not started yet, create any requested channels so scripts may set them at load time + if(!last_timestamp && channel_name){ + lua_channel(inst, (char*) channel_name, mmchannel_output); + } + //find correct channel & output value for(n = 0; n < data->channels; n++){ - if(!strcmp(channel_name, data->channel_name[n])){ - channel = mm_channel(inst, n, 0); - if(!channel){ - return 0; + if(!strcmp(channel_name, data->channel[n].name)){ + data->channel[n].out = val.normalised; + if(!last_timestamp){ + data->channel[n].mark = 1; + } + else{ + mm_channel_event(mm_channel(inst, n, 0), val); } - mm_channel_event(channel, val); - data->output[n] = val.normalised; return 0; } } @@ -189,6 +292,7 @@ static int lua_callback_interval(lua_State* interpreter){ if(lua_gettable(interpreter, LUA_REGISTRYINDEX) == LUA_TNUMBER){ //already interval'd reference = luaL_checkinteger(interpreter, 4); + DBGPF("Updating interval to %" PRIu64 " msec", interval); } else if(interval){ //get a reference to the function @@ -199,6 +303,8 @@ static int lua_callback_interval(lua_State* interpreter){ lua_pushvalue(interpreter, 1); lua_pushinteger(interpreter, reference); lua_settable(interpreter, LUA_REGISTRYINDEX); + + DBGPF("Registered interval with %" PRIu64 " msec", interval); } //find matching timer @@ -255,8 +361,8 @@ static int lua_callback_value(lua_State* interpreter, uint8_t input){ //find correct channel & return value for(n = 0; n < data->channels; n++){ - if(!strcmp(channel_name, data->channel_name[n])){ - lua_pushnumber(data->interpreter, (input) ? data->input[n] : data->output[n]); + if(!strcmp(channel_name, data->channel[n].name)){ + lua_pushnumber(interpreter, (input) ? data->channel[n].in : data->channel[n].out); return 1; } } @@ -273,6 +379,17 @@ static int lua_callback_output_value(lua_State* interpreter){ return lua_callback_value(interpreter, 0); } +static int lua_callback_input_channel(lua_State* interpreter){ + lua_pushstring(interpreter, LUA_REGISTRY_CURRENT_CHANNEL); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + return 1; +} + +static int lua_callback_timestamp(lua_State* interpreter){ + lua_pushnumber(interpreter, mm_timestamp()); + return 1; +} + static int lua_configure(char* option, char* value){ LOG("No backend configuration possible"); return 1; @@ -289,21 +406,21 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ } return 0; } + else if(!strcmp(option, "default-handler")){ + free(data->default_handler); + data->default_handler = strdup(value); + return 0; + } LOGPF("Unknown instance configuration parameter %s for instance %s", option, inst->name); return 1; } -static instance* lua_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int lua_instance(instance* inst){ lua_instance_data* data = calloc(1, sizeof(lua_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } //load the interpreter @@ -311,7 +428,7 @@ static instance* lua_instance(){ if(!data->interpreter){ LOG("Failed to initialize interpreter"); free(data); - return NULL; + return 1; } luaL_openlibs(data->interpreter); @@ -320,6 +437,10 @@ static instance* lua_instance(){ 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); + lua_register(data->interpreter, "input_channel", lua_callback_input_channel); + lua_register(data->interpreter, "timestamp", lua_callback_timestamp); + lua_register(data->interpreter, "thread", lua_callback_thread); + lua_register(data->interpreter, "sleep", lua_callback_sleep); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -327,7 +448,7 @@ static instance* lua_instance(){ lua_settable(data->interpreter, LUA_REGISTRYINDEX); inst->impl = data; - return inst; + return 0; } static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ @@ -336,26 +457,23 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ //find matching channel for(u = 0; u < data->channels; u++){ - if(!strcmp(spec, data->channel_name[u])){ + if(!strcmp(spec, data->channel[u].name)){ break; } } //allocate new channel if(u == data->channels){ - data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*)); - data->reference = realloc(data->reference, (u + 1) * sizeof(int)); - data->input = realloc(data->input, (u + 1) * sizeof(double)); - data->output = realloc(data->output, (u + 1) * sizeof(double)); - if(!data->channel_name || !data->reference || !data->input || !data->output){ + data->channel = realloc(data->channel, (data->channels + 1) * sizeof(lua_channel_data)); + if(!data->channel){ LOG("Failed to allocate memory"); + data->channels = 0; return NULL; } - data->reference[u] = LUA_NOREF; - data->input[u] = data->output[u] = 0.0; - data->channel_name[u] = strdup(spec); - if(!data->channel_name[u]){ + data->channel[u].in = data->channel[u].out = 0.0; + data->channel[u].name = strdup(spec); + if(!data->channel[u].name){ LOG("Failed to allocate memory"); return NULL; } @@ -366,22 +484,33 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ } static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ - size_t n = 0; + size_t n = 0, ident; lua_instance_data* data = (lua_instance_data*) inst->impl; //handle all incoming events for(n = 0; n < num; n++){ - data->input[c[n]->ident] = v[n].normalised; + ident = c[n]->ident; + data->channel[ident].in = v[n].normalised; //call lua channel handlers if present - if(data->reference[c[n]->ident] != LUA_NOREF){ - lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); + if(data->channel[ident].reference != LUA_NOREF){ + //push the channel name + lua_pushstring(data->interpreter, LUA_REGISTRY_CURRENT_CHANNEL); + lua_pushstring(data->interpreter, data->channel[ident].name); + lua_settable(data->interpreter, LUA_REGISTRYINDEX); + + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->channel[ident].reference); lua_pushnumber(data->interpreter, v[n].normalised); if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ - LOGPF("Failed to call handler for %s.%s: %s", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); + LOGPF("Failed to call handler for %s.%s: %s", inst->name, data->channel[ident].name, lua_tostring(data->interpreter, -1)); lua_pop(data->interpreter, 1); } } } + + //clear the channel name + lua_pushstring(data->interpreter, LUA_REGISTRY_CURRENT_CHANNEL); + lua_pushnil(data->interpreter); + lua_settable(data->interpreter, LUA_REGISTRYINDEX); return 0; } @@ -401,9 +530,6 @@ static int lua_handle(size_t num, managed_fd* fds){ return 1; } #else - if(!last_timestamp){ - last_timestamp = mm_timestamp(); - } delta = mm_timestamp() - last_timestamp; last_timestamp = mm_timestamp(); #endif @@ -422,30 +548,75 @@ static int lua_handle(size_t num, managed_fd* fds){ timer[n].delta %= timer[n].interval; lua_rawgeti(timer[n].interpreter, LUA_REGISTRYINDEX, timer[n].reference); lua_pcall(timer[n].interpreter, 0, 0, 0); + DBGPF("Calling interval timer function %" PRIsize_t, n); } } } + + //check for threads to wake up + for(n = 0; n < threads; n++){ + if(thread[n].timeout && delta >= thread[n].timeout){ + lua_thread_resume(n); + lua_update_timerfd(); + } + else if(thread[n].timeout){ + thread[n].timeout -= delta; + } + } return 0; } +static int lua_resolve_symbol(lua_State* interpreter, char* symbol){ + int reference = LUA_REFNIL; + + //exclude reserved names + if(!strcmp(symbol, "output") + || !strcmp(symbol, "thread") + || !strcmp(symbol, "sleep") + || !strcmp(symbol, "input_value") + || !strcmp(symbol, "output_value") + || !strcmp(symbol, "input_channel") + || !strcmp(symbol, "timestamp") + || !strcmp(symbol, "interval")){ + return LUA_NOREF; + } + + lua_getglobal(interpreter, symbol); + reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + if(reference == LUA_REFNIL){ + return LUA_NOREF; + } + return reference; +} + static int lua_start(size_t n, instance** inst){ size_t u, p; lua_instance_data* data = NULL; + int default_handler; + channel_value v; //resolve channels to their handler functions for(u = 0; u < n; u++){ data = (lua_instance_data*) inst[u]->impl; + default_handler = LUA_NOREF; + + //try to resolve default handler if given + if(data->default_handler){ + default_handler = lua_resolve_symbol(data->interpreter, data->default_handler); + if(default_handler == LUA_NOREF){ + LOGPF("Failed to resolve default handler %s on %s", data->default_handler, inst[u]->name); + } + } + for(p = 0; p < data->channels; p++){ - //exclude reserved names - if(strcmp(data->channel_name[p], "output") - && strcmp(data->channel_name[p], "input_value") - && strcmp(data->channel_name[p], "output_value") - && strcmp(data->channel_name[p], "interval")){ - lua_getglobal(data->interpreter, data->channel_name[p]); - data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); - if(data->reference[p] == LUA_REFNIL){ - data->reference[p] = LUA_NOREF; - } + data->channel[p].reference = default_handler; + if(!data->default_handler){ + data->channel[p].reference = lua_resolve_symbol(data->interpreter, data->channel[p].name); + } + //push initial values + if(data->channel[p].mark){ + v.normalised = data->channel[p].out; + mm_channel_event(mm_channel(inst[u], p, 0), v); } } } @@ -457,6 +628,7 @@ static int lua_start(size_t n, instance** inst){ return 1; } #endif + last_timestamp = mm_timestamp(); return 0; } @@ -470,12 +642,10 @@ static int lua_shutdown(size_t n, instance** inst){ lua_close(data->interpreter); //cleanup channel data for(p = 0; p < data->channels; p++){ - free(data->channel_name[p]); + free(data->channel[p].name); } - free(data->channel_name); - free(data->reference); - free(data->input); - free(data->output); + free(data->channel); + free(data->default_handler); free(inst[u]->impl); } @@ -483,6 +653,9 @@ static int lua_shutdown(size_t n, instance** inst){ free(timer); timer = NULL; timers = 0; + free(thread); + thread = NULL; + threads = 0; #ifdef MMBACKEND_LUA_TIMERFD close(timer_fd); timer_fd = -1; diff --git a/backends/lua.h b/backends/lua.h index 75f03c4..4583dfe 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -12,7 +12,7 @@ MM_PLUGIN_API int init(); static int lua_configure(char* option, char* value); static int lua_configure_instance(instance* inst, char* option, char* value); -static instance* lua_instance(); +static int lua_instance(instance* inst); 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); @@ -22,13 +22,20 @@ static int lua_shutdown(size_t n, instance** inst); static uint32_t lua_interval(); #endif +typedef struct /*_lua_channel*/ { + char* name; + int reference; + double in; + double out; + uint8_t mark; +} lua_channel_data; + typedef struct /*_lua_instance_data*/ { size_t channels; - char** channel_name; - int* reference; - double* input; - double* output; + lua_channel_data* channel; + lua_State* interpreter; + char* default_handler; } lua_instance_data; typedef struct /*_lua_interval_callback*/ { @@ -37,3 +44,10 @@ typedef struct /*_lua_interval_callback*/ { lua_State* interpreter; int reference; } lua_timer; + +typedef struct /*_lua_coroutine*/ { + instance* instance; + lua_State* thread; + int reference; + uint64_t timeout; +} lua_thread; diff --git a/backends/lua.md b/backends/lua.md index f38e189..05509b6 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -1,51 +1,71 @@ ### The `lua` backend -The `lua` backend provides a flexible programming environment, allowing users to route and manipulate -events using the Lua programming language. +The `lua` backend provides a flexible programming environment, allowing users to route, generate +and manipulate events using the Lua scripting language. -Every instance has it's own interpreter state which can be loaded with custom handler scripts. +Every instance has its own interpreter state which can be loaded with custom handler scripts. To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) -with the value (as a Lua `number` type) as parameter. +with the value (as a Lua `number` type) as parameter. Alternatively, a designated default channel handler +may be supplied in the configuration. The following functions are provided within the Lua interpreter for interaction with the MIDIMonster | 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) | +| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function | | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel | | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | - +| `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | +| `timestamp()` | `print(timestamp())` | Returns the core timestamp for this iteration with millisecond resolution. This is not a performance timer, but intended for timeouting, etc | +| `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) | +| `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds | Example script: -``` +```lua function bar(value) - output("foo", value / 2) + output("foo", value / 2); end step = 0 function toggle() - output("bar", step * 1.0) + output("bar", step * 1.0); step = (step + 1) % 2; end +function run_show() + while(true) do + sleep(1000); + output("narf", 0); + sleep(1000); + output("narf", 1.0); + end +end + interval(toggle, 1000) +thread(run_show) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. +Threads are implemented as Lua coroutines, not operating system threads. This means that +cooperative multithreading is required, which can be achieved by calling the `sleep(number)` +function from within a running thread. Calling that function from any other context is +not supported. + #### Global configuration The `lua` backend does not take any global configuration. #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `script` | `script.lua` | none | Lua source file (relative to configuration file)| +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------| +| `script` | `script.lua` | none | Lua source file (relative to configuration file) | +| `default-handler` | `handler` | none | Name of a function to be called as handler for all incoming channels (instead of the per-channel handlers) | -A single instance may have multiple `source` options specified, which will all be read cumulatively. +A single instance may have multiple `script` options specified, which will all be read cumulatively. #### Channel specification @@ -58,9 +78,21 @@ 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. +Using any of the interface functions (`output`, `interval`, etc.) as an input channel name to a +Lua instance will not call any handler functions. Using these names as arguments to the output and +value interface functions works as intended. When using a default handler, the default handler will +be called. Output values will not trigger corresponding input event handlers unless the channel is mapped -back in the MIDIMonster configuration. +back in the MIDIMonster configuration. This is intentional. + +To build (and run) the `lua` backend on Windows, a compiled version of the Lua 5.3 library is required. +For various reasons (legal, separations of concern, not wanting to ship binary data in the repository), +the MIDIMonster project can not provide this file within this repository. +You will need to acquire a copy of `lua53.dll`, for example by downloading it from the [luabinaries +project](http://luabinaries.sourceforge.net/download.html). + +To build the `lua` backend for Windows, place `lua53.dll` in a subdirectory `libs/` in the project root +and run `make lua.dll` inside the `backends/` directory. + +At runtime, Windows searches for the file in the same directory as `midimonster.exe`. diff --git a/backends/maweb.c b/backends/maweb.c index 39a6cb2..6861d75 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -15,10 +15,15 @@ #define WS_FLAG_FIN 0x80 #define WS_FLAG_MASK 0x80 +/* + * TODO handle peer close/unregister/reopen and fallback connections + */ + 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 uint64_t quiet_mode = 0; static maweb_command_key cmdline_keys[] = { {"PREV", 109, 0, 1}, {"SET", 108, 1, 0, 1}, {"NEXT", 110, 0, 1}, @@ -139,6 +144,10 @@ static int maweb_configure(char* option, char* value){ update_interval = strtoul(value, NULL, 10); return 0; } + else if(!strcmp(option, "quiet")){ + quiet_mode = strtoul(value, NULL, 10); + return 0; + } LOGPF("Unknown backend configuration option %s", option); return 1; @@ -205,16 +214,11 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){ return 1; } -static instance* maweb_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int maweb_instance(instance* inst){ maweb_instance_data* data = calloc(1, sizeof(maweb_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->fd = -1; @@ -222,12 +226,12 @@ static instance* maweb_instance(){ if(!data->buffer){ LOG("Failed to allocate memory"); free(data); - return NULL; + return 1; } data->allocated = MAWEB_RECV_CHUNK; inst->impl = data; - return inst; + return 0; } static channel* maweb_channel(instance* inst, char* spec, uint8_t flags){ @@ -462,7 +466,9 @@ static int maweb_request_playbacks(instance* inst){ size_t page_index = 0, view = 3, channel = 0, offsets[3], channel_offset, channels; if(updates_inflight){ - LOGPF("Skipping update request, %" PRIu64 " updates still inflight", updates_inflight); + if(quiet_mode < 1){ + LOGPF("Skipping update request, %" PRIu64 " updates still inflight - consider raising the interval time", updates_inflight); + } return 0; } @@ -593,7 +599,9 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le data->login = 0; return 0; } - LOGPF("Session id is now %" PRId64, data->session); + if(quiet_mode < 2){ + LOGPF("Session id is now %" PRId64, data->session); + } } if(json_obj_bool(payload, "forceLogin", 0)){ @@ -752,7 +760,7 @@ static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ 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; + ssize_t bytes_read, bytes_left = data->allocated - data->offset, bytes_handled = 0; if(bytes_left < 3){ data->buffer = realloc(data->buffer, (data->allocated + MAWEB_RECV_CHUNK) * sizeof(uint8_t)); diff --git a/backends/maweb.h b/backends/maweb.h index 50b777a..80835d9 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -3,7 +3,7 @@ MM_PLUGIN_API int init(); static int maweb_configure(char* option, char* value); static int maweb_configure_instance(instance* inst, char* option, char* value); -static instance* maweb_instance(); +static int maweb_instance(instance* inst); 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); diff --git a/backends/maweb.md b/backends/maweb.md index 45dc778..eddf1a5 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -1,7 +1,7 @@ ### 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). +instances (GrandMA2 / GrandMA2 OnPC / Dot2 / 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. @@ -19,6 +19,7 @@ Web Remote. Set a web remote password using the option below the activation sett | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|---------------------------------------------------------------| | `interval` | `100` | `50` | Query interval for input data polling (in msec) | +| `quiet` | `1` | `0` | Turn off some warning messages, for use by experts | #### Instance configuration @@ -32,7 +33,7 @@ Web Remote. Set a web remote password using the option below the activation sett The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are not visible on the main console. The `console` mode is only available with GrandMA2 remotes and injects key events -into the main console. This mode also supports additional hardkeys that are only available on GrandMA consoles. +into the main console. This mode also supports additional hardkeys that are only available on GrandMA2 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. diff --git a/backends/midi.c b/backends/midi.c index 11d759d..1f0f2d5 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -13,9 +13,7 @@ enum /*_midi_channel_type*/ { cc, pressure, aftertouch, - pitchbend, - nrpn, - sysmsg + pitchbend }; static struct { @@ -69,19 +67,14 @@ static int midi_configure(char* option, char* value){ return 1; } -static instance* midi_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int midi_instance(instance* inst){ inst->impl = calloc(1, sizeof(midi_instance_data)); if(!inst->impl){ LOG("Failed to allocate memory"); - return NULL; + return 1; } - return inst; + return 0; } static int midi_configure_instance(instance* inst, char* option, char* value){ @@ -116,39 +109,22 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ .label = 0 }; - //support deprecated syntax for a transition period... - uint8_t old_syntax = 0; - char* channel; - + char* channel = NULL; if(!strncmp(spec, "ch", 2)){ channel = spec + 2; if(!strncmp(spec, "channel", 7)){ channel = spec + 7; } } - else if(!strncmp(spec, "cc", 2)){ - ident.fields.type = cc; - channel = spec + 2; - old_syntax = 1; - } - else if(!strncmp(spec, "note", 4)){ - ident.fields.type = note; - channel = spec + 4; - old_syntax = 1; - } - else if(!strncmp(spec, "nrpn", 4)){ - ident.fields.type = nrpn; - channel = spec + 4; - old_syntax = 1; - } - else{ - LOGPF("Unknown control type in %s", spec); + + if(!channel){ + LOGPF("Invalid channel specification %s", spec); return NULL; } ident.fields.channel = strtoul(channel, &channel, 10); if(ident.fields.channel > 15){ - LOGPF("Channel out of range in spec %s", spec); + LOGPF("MIDI channel out of range in spec %s", spec); return NULL; } @@ -159,33 +135,27 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ //skip the period channel++; - if(!old_syntax){ - if(!strncmp(channel, "cc", 2)){ - ident.fields.type = cc; - channel += 2; - } - else if(!strncmp(channel, "note", 4)){ - ident.fields.type = note; - channel += 4; - } - else if(!strncmp(channel, "nrpn", 4)){ - ident.fields.type = nrpn; - channel += 4; - } - else if(!strncmp(channel, "pressure", 8)){ - ident.fields.type = pressure; - channel += 8; - } - else if(!strncmp(channel, "pitch", 5)){ - ident.fields.type = pitchbend; - } - else if(!strncmp(channel, "aftertouch", 10)){ - ident.fields.type = aftertouch; - } - else{ - LOGPF("Unknown control type in %s", spec); - return NULL; - } + if(!strncmp(channel, "cc", 2)){ + ident.fields.type = cc; + channel += 2; + } + else if(!strncmp(channel, "note", 4)){ + ident.fields.type = note; + channel += 4; + } + else if(!strncmp(channel, "pressure", 8)){ + ident.fields.type = pressure; + channel += 8; + } + else if(!strncmp(channel, "pitch", 5)){ + ident.fields.type = pitchbend; + } + else if(!strncmp(channel, "aftertouch", 10)){ + ident.fields.type = aftertouch; + } + else{ + LOGPF("Unknown control type in %s", spec); + return NULL; } ident.fields.control = strtoul(channel, NULL, 10); @@ -229,9 +199,6 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ case aftertouch: snd_seq_ev_set_chanpress(&ev, ident.fields.channel, v[u].normalised * 127.0); break; - case nrpn: - //FIXME set to nrpn output - break; } snd_seq_event_output(sequencer, &ev); @@ -241,6 +208,24 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ return 0; } +static char* midi_type_name(uint8_t type){ + switch(type){ + case none: + return "none"; + case note: + return "note"; + case cc: + return "cc"; + case pressure: + return "pressure"; + case aftertouch: + return "aftertouch"; + case pitchbend: + return "pitch"; + } + return "unknown"; +} + static int midi_handle(size_t num, managed_fd* fds){ snd_seq_event_t* ev = NULL; instance* inst = NULL; @@ -258,59 +243,45 @@ static int midi_handle(size_t num, managed_fd* fds){ while(snd_seq_event_input(sequencer, &ev) > 0){ event_type = NULL; ident.label = 0; + + ident.fields.channel = ev->data.note.channel; + ident.fields.control = ev->data.note.note; + val.normalised = (double) ev->data.note.velocity / 127.0; + switch(ev->type){ case SND_SEQ_EVENT_NOTEON: case SND_SEQ_EVENT_NOTEOFF: case SND_SEQ_EVENT_NOTE: ident.fields.type = note; - ident.fields.channel = ev->data.note.channel; - ident.fields.control = ev->data.note.note; - val.normalised = (double)ev->data.note.velocity / 127.0; if(ev->type == SND_SEQ_EVENT_NOTEOFF){ val.normalised = 0; } - event_type = "note"; break; case SND_SEQ_EVENT_KEYPRESS: ident.fields.type = pressure; - ident.fields.channel = ev->data.note.channel; - ident.fields.control = ev->data.note.note; - val.normalised = (double)ev->data.note.velocity / 127.0; - event_type = "pressure"; break; case SND_SEQ_EVENT_CHANPRESS: ident.fields.type = aftertouch; ident.fields.channel = ev->data.control.channel; - val.normalised = (double)ev->data.control.value / 127.0; - event_type = "aftertouch"; + val.normalised = (double) ev->data.control.value / 127.0; break; case SND_SEQ_EVENT_PITCHBEND: ident.fields.type = pitchbend; ident.fields.channel = ev->data.control.channel; - val.normalised = ((double)ev->data.control.value + 8192) / 16383.0; - event_type = "pitch"; + val.normalised = ((double) ev->data.control.value + 8192) / 16383.0; break; case SND_SEQ_EVENT_CONTROLLER: ident.fields.type = cc; ident.fields.channel = ev->data.control.channel; ident.fields.control = ev->data.control.param; - val.raw.u64 = ev->data.control.value; - val.normalised = (double)ev->data.control.value / 127.0; - event_type = "cc"; - break; - case SND_SEQ_EVENT_CONTROL14: - case SND_SEQ_EVENT_NONREGPARAM: - case SND_SEQ_EVENT_REGPARAM: - //FIXME value calculation - ident.fields.type = nrpn; - ident.fields.channel = ev->data.control.channel; - ident.fields.control = ev->data.control.param; + val.normalised = (double) ev->data.control.value / 127.0; break; default: LOG("Ignored event of unsupported type"); continue; } + event_type = midi_type_name(ident.fields.type); inst = mm_instance_find(BACKEND_NAME, ev->dest.port); if(!inst){ //FIXME might want to return failure diff --git a/backends/midi.h b/backends/midi.h index 66a02bc..dcee010 100644 --- a/backends/midi.h +++ b/backends/midi.h @@ -3,7 +3,7 @@ MM_PLUGIN_API int init(); static int midi_configure(char* option, char* value); static int midi_configure_instance(instance* instance, char* option, char* value); -static instance* midi_instance(); +static int midi_instance(instance* inst); 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); diff --git a/backends/midi.md b/backends/midi.md index 108860e..d3d6e33 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -30,12 +30,9 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages -* `nrpn` - NRPNs (not yet implemented) A MIDIMonster channel is specified using the syntax `channel<channel>.<type><index>`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). -The earlier syntax of `<type><channel>.<index>` is officially deprecated but still supported for compatibility -reasons. This support may be removed at some future time. The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel<channel>.<type>`. @@ -59,7 +56,5 @@ Currently, no Note Off messages are sent (instead, Note On messages with a veloc generated, which amount to the same thing according to the spec). This may be implemented as a configuration option at a later time. -NRPNs are not yet fully implemented, though rudimentary support is in the codebase. - To see which events your MIDI devices output, ALSA provides the `aseqdump` utility. You can list all incoming events using `aseqdump -p <portname>`. diff --git a/backends/ola.cpp b/backends/ola.cpp index 09d68c9..106dbd5 100644 --- a/backends/ola.cpp +++ b/backends/ola.cpp @@ -40,21 +40,15 @@ static int ola_configure(char* option, char* value){ 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)); +static int ola_instance(instance* inst){ + ola_instance_data* data = (ola_instance_data*) calloc(1, sizeof(ola_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } inst->impl = data; - return inst; + return 0; } static int ola_configure_instance(instance* inst, char* option, char* value){ @@ -188,7 +182,7 @@ static void ola_data_receive(unsigned int universe, const ola::DmxBuffer& ola_dm else{ chan = mm_channel(inst, p, 0); } - + if(!chan){ LOGPF("Active channel %" PRIsize_t " on %s not known to core", p, inst->name); return; diff --git a/backends/ola.h b/backends/ola.h index 083e971..68244ec 100644 --- a/backends/ola.h +++ b/backends/ola.h @@ -7,7 +7,7 @@ extern "C" { 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 int ola_instance(instance* inst); 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); diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c new file mode 100644 index 0000000..2a5e01f --- /dev/null +++ b/backends/openpixelcontrol.c @@ -0,0 +1,686 @@ +#define BACKEND_NAME "openpixelcontrol" + +#include <string.h> + +#include "libmmbackend.h" +#include "openpixelcontrol.h" + +/* + * TODO handle destination close/unregister/reopen + */ + +MM_PLUGIN_API int init(){ + backend openpixel = { + .name = BACKEND_NAME, + .conf = openpixel_configure, + .create = openpixel_instance, + .conf_instance = openpixel_configure_instance, + .channel = openpixel_channel, + .handle = openpixel_set, + .process = openpixel_handle, + .start = openpixel_start, + .shutdown = openpixel_shutdown + }; + + //register backend + if(mm_backend_register(openpixel)){ + LOG("Failed to register backend"); + return 1; + } + return 0; +} + +static int openpixel_configure(char* option, char* value){ + //no global configuration + LOG("No backend configuration possible"); + return 1; +} + +static int openpixel_configure_instance(instance* inst, char* option, char* value){ + char* host = NULL, *port = NULL; + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + + //FIXME this should store the destination/listen address and establish on _start + if(!strcmp(option, "destination")){ + mmbackend_parse_hostspec(value, &host, &port, NULL); + if(!host || !port){ + LOGPF("Invalid destination address specified for instance %s", inst->name); + return 1; + } + + data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0); + if(data->dest_fd >= 0){ + return 0; + } + LOGPF("Failed to connect to server for instance %s", inst->name); + return 1; + } + if(!strcmp(option, "listen")){ + mmbackend_parse_hostspec(value, &host, &port, NULL); + if(!host || !port){ + LOGPF("Invalid listen address specified for instance %s", inst->name); + return 1; + } + + data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0); + if(data->listen_fd >= 0 && !listen(data->listen_fd, SOMAXCONN)){ + return 0; + } + LOGPF("Failed to bind server descriptor for instance %s", inst->name); + return 1; + } + else if(!strcmp(option, "mode")){ + if(!strcmp(value, "16bit")){ + data->mode = rgb16; + return 0; + } + else if(!strcmp(value, "8bit")){ + data->mode = rgb8; + return 0; + } + LOGPF("Unknown instance mode %s\n", value); + return 1; + } + + LOGPF("Unknown instance option %s for instance %s", option, inst->name); + return 1; +} + +static int openpixel_instance(instance* inst){ + openpixel_instance_data* data = calloc(1, sizeof(openpixel_instance_data)); + inst->impl = data; + if(!inst->impl){ + LOG("Failed to allocate memory"); + return 1; + } + + data->dest_fd = -1; + data->listen_fd = -1; + return 0; +} + +static ssize_t openpixel_buffer_find(openpixel_instance_data* data, uint8_t strip, uint8_t input){ + ssize_t n = 0; + + for(n = 0; n < data->buffers; n++){ + if(data->buffer[n].strip == strip + && (data->buffer[n].flags & OPENPIXEL_INPUT) >= input){ + DBGPF("Using allocated %s buffer for requested strip %d, size %d", input ? "input" : "output", strip, data->buffer[n].bytes); + return n; + } + } + DBGPF("Instance has no %s buffer for requested strip %d", input ? "input" : "output", strip); + return -1; +} + +static int openpixel_buffer_extend(openpixel_instance_data* data, uint8_t strip, uint8_t input, uint16_t length){ + ssize_t buffer = openpixel_buffer_find(data, strip, input); + + //length is in component-channels, round it to the nearest rgb-triplet + //this guarantees that any allocated buffer has at least three bytes, which is important to parts of the receive handler + length = (length % 3) ? ((length / 3) + 1) * 3 : length; + + //calculate required buffer length + size_t bytes_required = (data->mode == rgb8) ? length : length * 2; + if(buffer < 0){ + //allocate new buffer + data->buffer = realloc(data->buffer, (data->buffers + 1) * sizeof(openpixel_buffer)); + if(!data->buffer){ + data->buffers = 0; + LOG("Failed to allocate memory"); + return -1; + } + + buffer = data->buffers; + data->buffers++; + + data->buffer[buffer].strip = strip; + data->buffer[buffer].flags = input ? OPENPIXEL_INPUT : 0; + data->buffer[buffer].bytes = 0; + data->buffer[buffer].data.u8 = NULL; + } + + if(data->buffer[buffer].bytes < bytes_required){ + //resize buffer + data->buffer[buffer].data.u8 = realloc(data->buffer[buffer].data.u8, bytes_required); + if(!data->buffer[buffer].data.u8){ + data->buffer[buffer].bytes = 0; + LOG("Failed to allocate memory"); + return 1; + } + //FIXME might want to memset() only newly allocated channels + memset(data->buffer[buffer].data.u8, 0, bytes_required); + data->buffer[buffer].bytes = bytes_required; + } + return 0; +} + +static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags){ + uint32_t strip = 0, channel = 0; + char* token = spec; + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + + //read strip index if supplied + if(!strncmp(spec, "strip", 5)){ + strip = strtoul(spec + 5, &token, 10); + //skip the dot + token++; + } + + //read (and calculate) channel index + if(!strncmp(token, "channel", 7)){ + channel = strtoul(token + 7, NULL, 10); + } + else if(!strncmp(token, "red", 3)){ + channel = strtoul(token + 3, NULL, 10) * 3 - 2; + } + else if(!strncmp(token, "green", 5)){ + channel = strtoul(token + 5, NULL, 10) * 3 - 1; + } + else if(!strncmp(token, "blue", 4)){ + channel = strtoul(token + 4, NULL, 10) * 3; + } + + if(!channel){ + LOGPF("Invalid channel specification %s", spec); + return NULL; + } + + //check channel direction + if(flags & mmchannel_input){ + //strip 0 (bcast) can not be mapped as input + if(!strip){ + LOGPF("Broadcast channel %s.%s can not be mapped as an input", inst->name, spec); + return NULL; + } + if(data->listen_fd < 0){ + LOGPF("Channel %s mapped as input, but instance %s is not accepting input", spec, inst->name); + return NULL; + } + + if(openpixel_buffer_extend(data, strip, 1, channel)){ + return NULL; + } + } + + if(flags & mmchannel_output){ + if(data->dest_fd < 0){ + LOGPF("Channel %s mapped as output, but instance %s is not sending output", spec, inst->name); + return NULL; + } + + if(openpixel_buffer_extend(data, strip, 0, channel)){ + return NULL; + } + } + + return mm_channel(inst, ((uint64_t) strip) << 32 | channel, 1); +} + +static int openpixel_output_data(openpixel_instance_data* data){ + size_t u; + openpixel_header hdr; + + //send updated strips + for(u = 0; u < data->buffers; u++){ + if(!(data->buffer[u].flags & OPENPIXEL_INPUT) && (data->buffer[u].flags & OPENPIXEL_MARK)){ + //remove mark + data->buffer[u].flags &= ~OPENPIXEL_MARK; + + //prepare header + hdr.strip = data->buffer[u].strip; + hdr.mode = data->mode; + hdr.length = htobe16(data->buffer[u].bytes); + + //output data + if(mmbackend_send(data->dest_fd, (uint8_t*) &hdr, sizeof(hdr)) + || mmbackend_send(data->dest_fd, data->buffer[u].data.u8, data->buffer[u].bytes)){ + return 1; + } + } + } + + return 0; +} + +static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v){ + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + size_t u, p; + ssize_t buffer; + uint32_t strip, channel; + + for(u = 0; u < num; u++){ + //read strip/channel + strip = c[u]->ident >> 32; + channel = c[u]->ident & 0xFFFFFFFF; + channel--; + + //find the buffer + buffer = openpixel_buffer_find(data, strip, 0); + if(buffer < 0){ + LOGPF("No buffer for channel %s.%d.%d\n", inst->name, strip, channel); + continue; + } + + //mark buffer for output + data->buffer[buffer].flags |= OPENPIXEL_MARK; + + //update data + switch(data->mode){ + case rgb8: + data->buffer[buffer].data.u8[channel] = ((uint8_t)(v[u].normalised * 255.0)); + break; + case rgb16: + data->buffer[buffer].data.u16[channel] = ((uint16_t)(v[u].normalised * 65535.0)); + break; + } + + if(strip == 0){ + //update values in all other output strips, don't mark + for(p = 0; p < data->buffers; p++){ + if(!(data->buffer[p].flags & OPENPIXEL_INPUT)){ + //check whether the buffer is large enough + if(data->mode == rgb8 && data->buffer[p].bytes >= channel){ + data->buffer[p].data.u8[channel] = ((uint8_t)(v[u].normalised * 255.0)); + } + else if(data->mode == rgb16 && data->buffer[p].bytes >= channel * 2){ + data->buffer[p].data.u16[channel] = ((uint16_t)(v[u].normalised * 65535.0)); + } + } + } + } + } + + return openpixel_output_data(data); +} + +static int openpixel_client_new(instance* inst, int fd){ + if(fd < 0){ + return 1; + } + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + size_t u; + + //mark nonblocking + #ifdef _WIN32 + unsigned long flags = 1; + if(ioctlsocket(fd, FIONBIO, &flags)){ + #else + int flags = fcntl(fd, F_GETFL, 0); + if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ + #endif + LOGPF("Failed to set client descriptor on %s nonblocking", inst->name); + close(fd); + return 0; + } + + //find a client block + for(u = 0; u < data->clients; u++){ + if(data->client[u].fd <= 0){ + break; + } + } + + //if no free slot, make one + if(u == data->clients){ + data->client = realloc(data->client, (data->clients + 1) * sizeof(openpixel_client)); + if(!data->client){ + data->clients = 0; + LOG("Failed to allocate memory"); + return 1; + } + data->clients++; + } + + data->client[u].fd = fd; + data->client[u].buffer = -1; + data->client[u].offset = 0; + + LOGPF("New client on instance %s", inst->name); + return mm_manage_fd(fd, BACKEND_NAME, 1, inst); +} + +static size_t openpixel_strip_pixeldata8(instance* inst, openpixel_client* client, uint8_t* data, openpixel_buffer* buffer, size_t bytes_left){ + channel* chan = NULL; + channel_value val; + size_t u; + + for(u = 0; u < bytes_left; u++){ + //if over buffer length, ignore + if(u + client->offset >= buffer->bytes){ + client->buffer = -2; + break; + } + + //FIXME if at start of trailing non-multiple of 3, ignore + + //update changed channels + if(buffer->data.u8[u + client->offset] != data[u]){ + buffer->data.u8[u + client->offset] = data[u]; + chan = mm_channel(inst, ((uint64_t) buffer->strip << 32) | (u + client->offset + 1), 0); + if(chan){ + //push event + val.raw.u64 = data[u]; + val.normalised = (double) data[u] / 255.0; + if(mm_channel_event(chan, val)){ + LOG("Failed to push channel event to core"); + } + } + } + } + return u; +} + +static size_t openpixel_strip_pixeldata16(instance* inst, openpixel_client* client, uint8_t* data, openpixel_buffer* buffer, size_t bytes_left){ + channel* chan = NULL; + channel_value val; + size_t u; + + for(u = 0; u < bytes_left; u++){ + //if over buffer length, ignore + if(u + client->offset >= buffer->bytes){ + client->buffer = -2; + break; + } + + //if at start of trailing non-multiple of 6, ignore + if((client->offset + u) >= (client->offset + client->left) - ((client->offset + client->left) % 6)){ + client->buffer = -2; + break; + } + + //byte-order conversion may be on message boundary, do it via a buffer + client->boundary.u8[(client->offset + u) % 2] = data[u]; + + //detect and update changed channels + if((client->offset + u) % 2 + && buffer->data.u16[(u + client->offset) / 2] != be16toh(client->boundary.u16)){ + buffer->data.u16[(u + client->offset) / 2] = be16toh(client->boundary.u16); + chan = mm_channel(inst, ((uint64_t) buffer->strip << 32) | ((u + client->offset) / 2 + 1), 0); + if(chan){ + //push event + val.raw.u64 = be16toh(client->boundary.u16);; + val.normalised = (double) val.raw.u64 / 65535.0; + if(mm_channel_event(chan, val)){ + LOG("Failed to push channel event to core"); + } + } + + } + } + return u; +} + +static ssize_t openpixel_client_pixeldata(instance* inst, openpixel_client* client, uint8_t* buffer, size_t bytes_left){ + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + openpixel_client temp_client = { + .fd = -1 + }; + ssize_t u, p; + uint8_t processing_done = 1; + + //ignore data + if(client->buffer == -2){ + //ignore data + client->offset += bytes_left; + client->left -= bytes_left; + return bytes_left; + } + //handle broadcast data + else if(client->buffer == -3){ + //iterate all input strips + for(p = 0; p < data->buffers; p++){ + if(data->buffer[p].flags & OPENPIXEL_INPUT){ + //prepare temporary client + temp_client.buffer = p; + temp_client.hdr = client->hdr; + temp_client.hdr.strip = data->buffer[p].strip; + temp_client.offset = client->offset; + temp_client.left = client->left; + + //run processing on strip + if(data->mode == rgb8){ + openpixel_strip_pixeldata8(inst, &temp_client, buffer, data->buffer + p, bytes_left); + } + else{ + openpixel_strip_pixeldata16(inst, &temp_client, buffer, data->buffer + p, bytes_left); + } + if(temp_client.buffer != -2){ + processing_done = 0; + } + } + } + + //if all strips report being done, ignore the rest of the data + if(processing_done){ + client->buffer = -2; + } + + //remove data + u = min(client->left, bytes_left); + client->offset += u; + client->left -= u; + return u; + } + //process data + else{ + if(data->mode == rgb8){ + u = openpixel_strip_pixeldata8(inst, client, buffer, data->buffer + client->buffer, bytes_left); + } + else{ + u = openpixel_strip_pixeldata16(inst, client, buffer, data->buffer + client->buffer, bytes_left); + } + + //update offsets + client->offset += u; + client->left -= u; + return u; + } + return -1; +} + +static ssize_t openpixel_client_headerdata(instance* inst, openpixel_client* client, uint8_t* buffer, size_t bytes_left){ + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + size_t bytes_consumed = min(sizeof(openpixel_header) - client->offset, bytes_left); + + DBGPF("Reading %" PRIsize_t " bytes to header at offset %" PRIsize_t ", header size %" PRIsize_t ", %" PRIsize_t " bytes left", bytes_consumed, client->offset, sizeof(openpixel_header), bytes_left); + memcpy(((uint8_t*) (&client->hdr)) + client->offset, buffer, bytes_consumed); + + //if done, resolve buffer + if(sizeof(openpixel_header) - client->offset <= bytes_left){ + //if broadcast strip, mark broadcast + if(client->hdr.strip == 0 + && data->mode == client->hdr.mode){ + client->buffer = -3; + } + else{ + client->buffer = openpixel_buffer_find(data, client->hdr.strip, 1); + //if no buffer or mode mismatch, ignore data + if(client->buffer < 0 + || data->mode != client->hdr.mode){ + client->buffer = -2; //mark for ignore + } + } + client->left = be16toh(client->hdr.length); + client->offset = 0; + } + //if not, update client offset + else{ + client->offset += bytes_consumed; + } + + //update scan offset + return bytes_consumed; +} + +static int openpixel_client_handle(instance* inst, int fd){ + openpixel_instance_data* data = (openpixel_instance_data*) inst->impl; + uint8_t buffer[8192]; + size_t c = 0, offset = 0; + ssize_t bytes_left = 0, bytes_handled; + + for(c = 0; c < data->clients; c++){ + if(data->client[c].fd == fd){ + break; + } + } + + if(c == data->clients){ + LOGPF("Unknown client descriptor signaled on %s", inst->name); + return 1; + } + + //FIXME might want to read until EAGAIN + ssize_t bytes = recv(fd, buffer, sizeof(buffer), 0); + if(bytes <= 0){ + if(bytes < 0){ + LOGPF("Failed to receive from client: %s", strerror(errno)); + } + + //close the connection + close(fd); + data->client[c].fd = -1; + + //unmanage the fd + LOGPF("Client disconnected on %s", inst->name); + mm_manage_fd(fd, BACKEND_NAME, 0, NULL); + return 0; + } + DBGPF("Received %" PRIsize_t " bytes on %s", bytes, inst->name); + + for(bytes_left = bytes - offset; bytes_left > 0; bytes_left = bytes - offset){ + if(data->client[c].buffer == -1){ + //read a header + bytes_handled = openpixel_client_headerdata(inst, data->client + c, buffer + offset, bytes_left); + if(bytes_handled < 0){ + //FIXME handle errors + } + } + else{ + //read data + bytes_handled = openpixel_client_pixeldata(inst, data->client + c, buffer + offset, min(bytes_left, data->client[c].left)); + if(bytes_handled < 0){ + //FIXME handle errors + } + + //end of data, return to reading headers + if(data->client[c].left == 0){ + data->client[c].buffer = -1; + data->client[c].offset = 0; + data->client[c].left = 0; + } + } + offset += bytes_handled; + } + DBGPF("Processing done on %s", inst->name); + + return 0; +} + +static int openpixel_handle(size_t num, managed_fd* fds){ + size_t u; + instance* inst = NULL; + openpixel_instance_data* data = NULL; + uint8_t buffer[8192]; + ssize_t bytes; + + for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + data = (openpixel_instance_data*) inst->impl; + + if(fds[u].fd == data->dest_fd){ + //destination fd ready to read + //since the protocol does not define any responses, the connection was probably closed + bytes = recv(data->dest_fd, buffer, sizeof(buffer), 0); + if(bytes <= 0){ + LOGPF("Output descriptor closed on instance %s", inst->name); + //unmanage the fd to give the core some rest + mm_manage_fd(data->dest_fd, BACKEND_NAME, 0, NULL); + } + else{ + LOGPF("Unhandled response data on %s (%" PRIsize_t" bytes)", inst->name, bytes); + } + } + else if(fds[u].fd == data->listen_fd){ + //listen fd ready to read, accept a new client + if(openpixel_client_new(inst, accept(data->listen_fd, NULL, NULL))){ + return 1; + } + } + else{ + //handle client input + if(openpixel_client_handle(inst, fds[u].fd)){ + return 1; + } + } + } + return 0; +} + +static int openpixel_start(size_t n, instance** inst){ + int rv = -1; + size_t u, nfds = 0; + openpixel_instance_data* data = NULL; + + for(u = 0; u < n; u++){ + data = (openpixel_instance_data*) inst[u]->impl; + + //register fds + if(data->dest_fd >= 0){ + if(mm_manage_fd(data->dest_fd, BACKEND_NAME, 1, inst[u])){ + LOGPF("Failed to register destination descriptor for instance %s with core", inst[u]->name); + goto bail; + } + nfds++; + } + if(data->listen_fd >= 0){ + if(mm_manage_fd(data->listen_fd, BACKEND_NAME, 1, inst[u])){ + LOGPF("Failed to register host descriptor for instance %s with core", inst[u]->name); + goto bail; + } + nfds++; + } + } + + LOGPF("Registered %" PRIsize_t " descriptors to core", nfds); + rv = 0; +bail: + return rv; +} + +static int openpixel_shutdown(size_t n, instance** inst){ + size_t u, p; + openpixel_instance_data* data = NULL; + + for(u = 0; u < n; u++){ + data = (openpixel_instance_data*) inst[u]->impl; + + //shutdown all clients + for(p = 0; p < data->clients; p++){ + if(data->client[p].fd>= 0){ + close(data->client[p].fd); + } + } + free(data->client); + + //close all configured fds + if(data->listen_fd >= 0){ + close(data->listen_fd); + } + if(data->dest_fd >= 0){ + close(data->dest_fd); + } + + //free all buffers + for(p = 0; p < data->buffers; p++){ + free(data->buffer[p].data.u8); + } + free(data->buffer); + + free(data); + inst[u]->impl = NULL; + } + + LOG("Backend shut down"); + return 0; +} diff --git a/backends/openpixelcontrol.h b/backends/openpixelcontrol.h new file mode 100644 index 0000000..63e9664 --- /dev/null +++ b/backends/openpixelcontrol.h @@ -0,0 +1,59 @@ +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static int openpixel_configure(char* option, char* value); +static int openpixel_configure_instance(instance* inst, char* option, char* value); +static int openpixel_instance(instance* inst); +static channel* openpixel_channel(instance* inst, char* spec, uint8_t flags); +static int openpixel_set(instance* inst, size_t num, channel** c, channel_value* v); +static int openpixel_handle(size_t num, managed_fd* fds); +static int openpixel_start(size_t n, instance** inst); +static int openpixel_shutdown(size_t n, instance** inst); + +#define OPENPIXEL_INPUT 1 +#define OPENPIXEL_MARK 2 + +typedef struct /*_data_buffer*/ { + uint8_t strip; + uint8_t flags; + uint16_t bytes; + union { + uint16_t* u16; + uint8_t* u8; + } data; +} openpixel_buffer; + +#pragma pack(push, 1) +typedef struct /*_openpixel_hdr*/ { + uint8_t strip; + uint8_t mode; + uint16_t length; +} openpixel_header; +#pragma pack(pop) + +typedef struct /*_openpixel_client*/ { + int fd; + ssize_t buffer; /* -1 header, -2 ignore, -3 bcast */ + openpixel_header hdr; + size_t offset; + size_t left; + union { + uint8_t u8[2]; + uint16_t u16; + } boundary; +} openpixel_client; + +typedef struct { + enum { + rgb8 = 0, + rgb16 = 2 + } mode; + + size_t buffers; + openpixel_buffer* buffer; + + int dest_fd; + int listen_fd; + size_t clients; + openpixel_client* client; +} openpixel_instance_data; diff --git a/backends/openpixelcontrol.md b/backends/openpixelcontrol.md new file mode 100644 index 0000000..d09d412 --- /dev/null +++ b/backends/openpixelcontrol.md @@ -0,0 +1,55 @@ +### The `openpixelcontrol` backend + +This backend provides read-write access to the TCP-based OpenPixelControl protocol, +used for controlling intelligent RGB led strips. + +This backend can both control a remote OpenPixelControl server as well as receive data +from OpenPixelControl clients. + +#### Global configuration + +This backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `destination` | `10.11.12.1 9001` | none | Destination for output data. Setting this option enables the instance for output | +| `listen` | `10.11.12.2 9002` | none | Local address to wait for client connections on. Setting this enables the instance for input | +| `mode` | `16bit` | `8bit` | RGB channel resolution | + +#### Channel specification + +Each instance can control up to 255 strips of RGB LED lights. The OpenPixelControl specification +confusingly calls these strips "channels". + +Strip `0` acts as a "broadcast" strip, setting values on all other strips at once. +Consequently, components on strip 0 can only be mapped as output channels to a destination +(setting components on all strips there), not as input channels. When such messages are received from +a client, the corresponding mapped component channels on all strips will receive events. + +Every single component of any LED on any string can be mapped as an individual MIDIMonster channel. +The components are laid out as sequences of Red - Green - Blue value triplets. + +Channels can be specified by their sequential index (one-based). + +Example mapping (data from Strip 2 LED 66's green component is mapped to the blue component of LED 2 on strip 1): +``` +strip1.channel6 < strip2.channel200 +``` + +Additionally, channels may be referred to by their color component and LED index: +``` +strip1.blue2 < strip2.green66 +``` + +#### Known bugs / problems + +If the connection is lost, it is currently not reestablished and may cause the MIDIMonster to exit entirely. +This behaviour may be changed in future releases. + +While acting as an OpenPixelControl server, the backend allows multiple clients to connect. +This may lead to confusing data output when multiple clients are trying to control the same strip. + +When acting as a 16bit OpenPixelControl server, input on the broadcast strip (strip 0) may cause erratic +value events on a few channels, especially with longer strips and inputs. diff --git a/backends/osc.c b/backends/osc.c index 7b9a5a5..754c290 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -562,21 +562,16 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ return 1; } -static instance* osc_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } - +static int osc_instance(instance* inst){ osc_instance_data* data = calloc(1, sizeof(osc_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->fd = -1; inst->impl = data; - return inst; + return 0; } static channel* osc_map_channel(instance* inst, char* spec, uint8_t flags){ diff --git a/backends/osc.h b/backends/osc.h index f8ff3ff..ec75e3f 100644 --- a/backends/osc.h +++ b/backends/osc.h @@ -10,7 +10,7 @@ MM_PLUGIN_API int init(); static int osc_configure(char* option, char* value); static int osc_configure_instance(instance* inst, char* option, char* value); -static instance* osc_instance(); +static int osc_instance(instance* inst); 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); diff --git a/backends/python.c b/backends/python.c new file mode 100644 index 0000000..9f1d642 --- /dev/null +++ b/backends/python.c @@ -0,0 +1,733 @@ +#define BACKEND_NAME "python" + +#define PY_SSIZE_T_CLEAN +#include <string.h> +#include <Python.h> +#include "python.h" + +#define MMPY_INSTANCE_KEY "midimonster_instance" + +static PyThreadState* python_main = NULL; +static wchar_t* program_name = NULL; + +static uint64_t last_timestamp = 0; +static uint32_t timer_interval = 0; +static size_t intervals = 0; +static mmpy_timer* interval = NULL; + +MM_PLUGIN_API int init(){ + backend python = { + .name = BACKEND_NAME, + .conf = python_configure, + .create = python_instance, + .conf_instance = python_configure_instance, + .channel = python_channel, + .handle = python_set, + .process = python_handle, + .start = python_start, + .interval = python_interval, + .shutdown = python_shutdown + }; + + //register backend + if(mm_backend_register(python)){ + LOG("Failed to register backend"); + return 1; + } + return 0; +} + +static uint32_t python_interval(){ + size_t u = 0; + uint32_t next_timer = 1000; + + if(timer_interval){ + for(u = 0; u < intervals; u++){ + if(interval[u].interval && + interval[u].interval - interval[u].delta < next_timer){ + next_timer = interval[u].interval - interval[u].delta; + } + } + DBGPF("Next timer fires in %" PRIu32, next_timer); + return next_timer; + } + + return 1000; +} + +static void python_timer_recalculate(){ + uint64_t next_interval = 0, gcd, residual; + size_t u; + + //find lower interval bounds + for(u = 0; u < intervals; u++){ + if(interval[u].interval && (!next_interval || interval[u].interval < next_interval)){ + next_interval = interval[u].interval; + } + } + + if(next_interval){ + for(u = 0; u < intervals; u++){ + if(interval[u].interval){ + //calculate gcd of current interval and this timers interval + gcd = interval[u].interval; + while(gcd){ + residual = next_interval % gcd; + next_interval = gcd; + gcd = residual; + } + + //10msec is absolute lower limit and minimum gcd due to rounding + if(next_interval <= 10){ + next_interval = 10; + break; + } + } + } + } + + timer_interval = next_interval; +} + +static int python_configure(char* option, char* value){ + LOG("No backend configuration possible"); + return 1; +} + +static int python_prepend_str(PyObject* list, char* str){ + if(!list || !str){ + return 1; + } + + PyObject* item = PyUnicode_FromString(str); + if(!item){ + return 1; + } + + if(PyList_Insert(list, 0, item) < 0){ + Py_DECREF(item); + return 1; + } + Py_DECREF(item); + return 0; +} + +static PyObject* mmpy_output(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + const char* channel_name = NULL; + channel_value val = { + {0} + }; + size_t u; + + if(!PyArg_ParseTuple(args, "sd", &channel_name, &val.normalised)){ + return NULL; + } + + val.normalised = clamp(val.normalised, 1.0, 0.0); + //if not started yet, create any requested channels so we can set them at load time + if(!last_timestamp){ + python_channel(inst, (char*) channel_name, mmchannel_output); + } + + for(u = 0; u < data->channels; u++){ + if(!strcmp(data->channel[u].name, channel_name)){ + DBGPF("Setting channel %s.%s to %f", inst->name, channel_name, val.normalised); + data->channel[u].out = val.normalised; + if(!last_timestamp){ + data->channel[u].mark = 1; + } + else{ + mm_channel_event(mm_channel(inst, u, 0), val); + } + return 0; + } + } + + if(u == data->channels){ + DBGPF("Output on unknown channel %s.%s, no event pushed", inst->name, channel_name); + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* mmpy_channel_value(PyObject* self, PyObject* args, uint8_t in){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + const char* channel_name = NULL; + size_t u; + + if(!PyArg_ParseTuple(args, "s", &channel_name)){ + return NULL; + } + + for(u = 0; u < data->channels; u++){ + if(!strcmp(data->channel[u].name, channel_name)){ + return PyFloat_FromDouble(in ? data->channel[u].in : data->channel[u].out); + } + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* mmpy_current_handler(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + + if(data->current_channel){ + return PyUnicode_FromString(data->current_channel->name); + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* mmpy_output_value(PyObject* self, PyObject* args){ + return mmpy_channel_value(self, args, 0); +} + +static PyObject* mmpy_input_value(PyObject* self, PyObject* args){ + return mmpy_channel_value(self, args, 1); +} + +static PyObject* mmpy_timestamp(PyObject* self, PyObject* args){ + return PyLong_FromUnsignedLong(mm_timestamp()); +} + +static PyObject* mmpy_interval(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + unsigned long updated_interval = 0; + PyObject* reference = NULL; + size_t u; + + if(!PyArg_ParseTuple(args, "Ok", &reference, &updated_interval)){ + return NULL; + } + + if(!PyCallable_Check(reference)){ + PyErr_SetString(PyExc_TypeError, "interval() requires a callable"); + return NULL; + } + + //round interval + if(updated_interval % 10 < 5){ + updated_interval -= updated_interval % 10; + } + else{ + updated_interval += (10 - (updated_interval % 10)); + } + + //find reference + for(u = 0; u < intervals; u++){ + if(interval[u].interpreter == data->interpreter + && PyObject_RichCompareBool(reference, interval[u].reference, Py_EQ) == 1){ + DBGPF("Updating interval to %" PRIu64 " msec", updated_interval); + break; + } + } + + //register new interval + if(u == intervals && updated_interval){ + //create new interval slot + DBGPF("Registering interval with %" PRIu64 " msec", updated_interval); + interval = realloc(interval, (intervals + 1) * sizeof(mmpy_timer)); + if(!interval){ + intervals = 0; + LOG("Failed to allocate memory"); + return NULL; + } + Py_INCREF(reference); + interval[intervals].delta = 0; + interval[intervals].reference = reference; + interval[intervals].interpreter = data->interpreter; + intervals++; + } + + //update if existing or created + if(u < intervals){ + interval[u].interval = updated_interval; + python_timer_recalculate(); + } + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + PyObject* handler = NULL, *sock = NULL, *fileno = NULL; + size_t u = 0, last_free = 0; + int fd = -1; + + if(!PyArg_ParseTuple(args, "OO", &handler, &sock)){ + return NULL; + } + + if(handler != Py_None && !PyCallable_Check(handler)){ + PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable"); + return NULL; + } + + fileno = PyObject_CallMethod(sock, "fileno", NULL); + if(!fileno || fileno == Py_None || !PyLong_Check(fileno)){ + PyErr_SetString(PyExc_TypeError, "manage() requires a socket-like object"); + return NULL; + } + + fd = PyLong_AsLong(fileno); + if(fd < 0){ + PyErr_SetString(PyExc_TypeError, "manage() requires a (connected) socket-like object"); + return NULL; + } + + //check if this socket instance was already registered + last_free = data->sockets; + for(u = 0; u < data->sockets; u++){ + if(!data->socket[u].socket){ + last_free = u; + } + else if(PyObject_RichCompareBool(sock, data->socket[u].socket, Py_EQ) == 1){ + break; + } + } + + if(u < data->sockets){ + //modify existing socket + Py_XDECREF(data->socket[u].handler); + if(handler != Py_None){ + DBGPF("Updating handler for fd %d on %s", fd, inst->name); + data->socket[u].handler = handler; + Py_INCREF(handler); + } + else{ + DBGPF("Unregistering fd %d on %s", fd, inst->name); + mm_manage_fd(data->socket[u].fd, BACKEND_NAME, 0, NULL); + Py_XDECREF(data->socket[u].socket); + data->socket[u].handler = NULL; + data->socket[u].socket = NULL; + data->socket[u].fd = -1; + } + } + else if(handler != Py_None){ + //check that the fd is not already registered with another socket instance + for(u = 0; u < data->sockets; u++){ + if(data->socket[u].fd == fd){ + //FIXME this might also raise an exception + LOGPF("Descriptor already registered with another socket on instance %s", inst->name); + Py_INCREF(Py_None); + return Py_None; + } + } + + DBGPF("Registering new fd %d on %s", fd, inst->name); + if(last_free == data->sockets){ + //allocate a new socket instance + data->socket = realloc(data->socket, (data->sockets + 1) * sizeof(mmpy_socket)); + if(!data->socket){ + data->sockets = 0; + LOG("Failed to allocate memory"); + return NULL; + } + data->sockets++; + } + + //store new reference + //FIXME check this for errors + mm_manage_fd(fd, BACKEND_NAME, 1, inst); + data->socket[last_free].fd = fd; + Py_INCREF(handler); + data->socket[last_free].handler = handler; + Py_INCREF(sock); + data->socket[last_free].socket = sock; + } + + Py_INCREF(Py_None); + return Py_None; +} + +static int mmpy_exec(PyObject* module) { + instance** inst = (instance**) PyModule_GetState(module); + //FIXME actually use interpreter dict (from python 3.8) here at some point + PyObject* capsule = PyDict_GetItemString(PyThreadState_GetDict(), MMPY_INSTANCE_KEY); + if(capsule && inst){ + *inst = PyCapsule_GetPointer(capsule, NULL); + return 0; + } + + PyErr_SetString(PyExc_AssertionError, "Failed to pass instance pointer for initialization"); + return -1; +} + +static int python_configure_instance(instance* inst, char* option, char* value){ + python_instance_data* data = (python_instance_data*) inst->impl; + PyObject* module = NULL; + + //load python script + if(!strcmp(option, "module")){ + //swap to interpreter + PyEval_RestoreThread(data->interpreter); + //import the module + module = PyImport_ImportModule(value); + if(!module){ + LOGPF("Failed to import module %s to instance %s", value, inst->name); + PyErr_Print(); + } + Py_XDECREF(module); + PyEval_ReleaseThread(data->interpreter); + return 0; + } + else if(!strcmp(option, "default-handler")){ + free(data->default_handler); + data->default_handler = strdup(value); + return 0; + } + + LOGPF("Unknown instance parameter %s for instance %s", option, inst->name); + return 1; +} + +static PyObject* mmpy_init(){ + static PyModuleDef_Slot mmpy_slots[] = { + {Py_mod_exec, (void*) mmpy_exec}, + {0} + }; + + static PyMethodDef mmpy_methods[] = { + {"output", mmpy_output, METH_VARARGS, "Output a channel event"}, + {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel"}, + {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel"}, + {"current", mmpy_current_handler, METH_VARARGS, "Get the name of the currently executing channel handler"}, + {"timestamp", mmpy_timestamp, METH_VARARGS, "Get the core timestamp (in milliseconds)"}, + {"manage", mmpy_manage_fd, METH_VARARGS, "(Un-)register a socket or file descriptor for notifications"}, + {"interval", mmpy_interval, METH_VARARGS, "Register or update an interval handler"}, + {0} + }; + + static struct PyModuleDef mmpy = { + PyModuleDef_HEAD_INIT, + "midimonster", + NULL, /*doc size*/ + sizeof(instance*), + mmpy_methods, + mmpy_slots + }; + + //single-phase init + //return PyModule_Create(&mmpy); + + //multi-phase init + return PyModuleDef_Init(&mmpy); +} + +static int python_instance(instance* inst){ + python_instance_data* data = calloc(1, sizeof(python_instance_data)); + PyObject* interpreter_dict = NULL; + char current_directory[8192]; + if(!data){ + LOG("Failed to allocate memory"); + return 1; + } + + //lazy-init because we need the interpreter running before _start, + //but don't want it running if no instances are defined + if(!python_main){ + LOG("Initializing main python interpreter"); + if(PyImport_AppendInittab("midimonster", &mmpy_init)){ + LOG("Failed to extend python inittab for main interpreter"); + } + program_name = Py_DecodeLocale("midimonster", NULL); + Py_SetProgramName(program_name); + //initialize python + Py_InitializeEx(0); + //create, acquire and release the GIL + PyEval_InitThreads(); + python_main = PyEval_SaveThread(); + } + + //acquire the GIL before creating a new interpreter + PyEval_RestoreThread(python_main); + //create subinterpreter for new instance + data->interpreter = Py_NewInterpreter(); + + //push cwd as import path + if(getcwd(current_directory, sizeof(current_directory))){ + if(python_prepend_str(PySys_GetObject("path"), current_directory)){ + LOG("Failed to push current working directory to python"); + goto bail; + } + } + + //push the instance pointer for later module initialization + //FIXME python 3.8 introduces interpreter_dict = PyInterpreterState_GetDict(data->interpreter->interp); + //for now use thread state... + interpreter_dict = PyThreadState_GetDict(); + if(!interpreter_dict){ + LOG("Failed to access per-interpreter data storage"); + goto bail; + } + //FIXME this might leak a reference to the capsule + if(PyDict_SetItemString(interpreter_dict, MMPY_INSTANCE_KEY, PyCapsule_New(inst, NULL, NULL))){ + LOG("Failed to set per-interpreter instance pointer"); + goto bail; + } + + //NewInterpreter leaves us with the GIL, drop it + PyEval_ReleaseThread(data->interpreter); + inst->impl = data; + return 0; + +bail: + if(data->interpreter){ + PyEval_ReleaseThread(data->interpreter); + } + free(data); + return 1; +} + +static channel* python_channel(instance* inst, char* spec, uint8_t flags){ + python_instance_data* data = (python_instance_data*) inst->impl; + size_t u; + + for(u = 0; u < data->channels; u++){ + if(!strcmp(data->channel[u].name, spec)){ + break; + } + } + + if(u == data->channels){ + data->channel = realloc(data->channel, (data->channels + 1) * sizeof(mmpython_channel)); + if(!data->channel){ + data->channels = 0; + LOG("Failed to allocate memory"); + return NULL; + } + memset(data->channel + u, 0, sizeof(mmpython_channel)); + + data->channel[u].name = strdup(spec); + if(!data->channel[u].name){ + LOG("Failed to allocate memory"); + return NULL; + } + data->channels++; + } + + return mm_channel(inst, u, 1); +} + +static int python_set(instance* inst, size_t num, channel** c, channel_value* v){ + python_instance_data* data = (python_instance_data*) inst->impl; + mmpython_channel* chan = NULL; + PyObject* result = NULL; + size_t u; + + //swap to interpreter + PyEval_RestoreThread(data->interpreter); + + for(u = 0; u < num; u++){ + chan = data->channel + c[u]->ident; + + //update input value buffer + chan->in = v[u].normalised; + + //call handler if present + if(chan->handler){ + DBGPF("Calling handler for %s.%s", inst->name, chan->name); + data->current_channel = chan; + result = PyObject_CallFunction(chan->handler, "d", chan->in); + Py_XDECREF(result); + data->current_channel = NULL; + DBGPF("Done with handler for %s.%s", inst->name, chan->name); + } + } + + //release interpreter + PyEval_ReleaseThread(data->interpreter); + return 0; +} + +static int python_handle(size_t num, managed_fd* fds){ + instance* inst = NULL; + python_instance_data* data = NULL; + PyObject* result = NULL; + size_t u, p; + + //handle intervals + if(timer_interval){ + uint64_t delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); + + //add delta to all active timers + for(u = 0; u < intervals; u++){ + if(interval[u].interval){ + interval[u].delta += delta; + + //if timer expired, call handler + if(interval[u].delta >= interval[u].interval){ + interval[u].delta %= interval[u].interval; + + //swap to interpreter + PyEval_RestoreThread(interval[u].interpreter); + //call handler + result = PyObject_CallFunction(interval[u].reference, NULL); + Py_XDECREF(result); + //release interpreter + PyEval_ReleaseThread(interval[u].interpreter); + DBGPF("Calling interval handler %" PRIsize_t, u); + } + } + } + } + + for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + data = (python_instance_data*) inst->impl; + + //swap to interpreter + PyEval_RestoreThread(data->interpreter); + + //handle callbacks + for(p = 0; p < data->sockets; p++){ + if(data->socket[p].socket + && data->socket[p].fd == fds[u].fd){ + //FIXME maybe close/unregister the socket on handling errors + DBGPF("Calling descriptor handler on %s for fd %d", inst->name, data->socket[p].fd); + result = PyObject_CallFunction(data->socket[p].handler, "O", data->socket[p].socket); + Py_XDECREF(result); + } + } + + //release interpreter + PyEval_ReleaseThread(data->interpreter); + } + + return 0; +} + +static PyObject* python_resolve_symbol(char* spec_raw){ + char* module_name = NULL, *object_name = NULL, *spec = strdup(spec_raw); + PyObject* module = NULL, *result = NULL; + + module = PyImport_AddModule("__main__"); + object_name = spec; + module_name = strchr(object_name, '.'); + if(module_name){ + *module_name = 0; + //returns borrowed reference + module = PyImport_AddModule(object_name); + + if(!module){ + LOGPF("Module %s for symbol %s.%s is not loaded", object_name, object_name, module_name + 1); + return NULL; + } + + object_name = module_name + 1; + + //returns new reference + result = PyObject_GetAttrString(module, object_name); + } + + free(spec); + return result; +} + +static int python_start(size_t n, instance** inst){ + python_instance_data* data = NULL; + size_t u, p; + channel_value v; + + //resolve channel references to handler functions + for(u = 0; u < n; u++){ + data = (python_instance_data*) inst[u]->impl; + DBGPF("Starting up instance %s", inst[u]->name); + + //switch to interpreter + PyEval_RestoreThread(data->interpreter); + + if(data->default_handler){ + data->handler = python_resolve_symbol(data->default_handler); + } + + for(p = 0; p < data->channels; p++){ + if(!strchr(data->channel[p].name, '.') && data->handler){ + data->channel[p].handler = data->handler; + } + else{ + data->channel[p].handler = python_resolve_symbol(data->channel[p].name); + } + //push initial values + if(data->channel[p].mark){ + v.normalised = data->channel[p].out; + mm_channel_event(mm_channel(inst[u], p, 0), v); + } + } + + //release interpreter + PyEval_ReleaseThread(data->interpreter); + } + return 0; +} + +static int python_shutdown(size_t n, instance** inst){ + size_t u, p; + python_instance_data* data = NULL; + + //clean up channels + //this needs to be done before stopping the interpreters, + //because the handler references are refcounted + for(u = 0; u < n; u++){ + data = (python_instance_data*) inst[u]->impl; + for(p = 0; p < data->channels; p++){ + free(data->channel[p].name); + Py_XDECREF(data->channel[p].handler); + } + free(data->channel); + free(data->default_handler); + //do not free data here, needed for shutting down interpreters + } + + if(python_main){ + //just used to lock the GIL + PyEval_RestoreThread(python_main); + + for(u = 0; u < n; u++){ + data = (python_instance_data*) inst[u]->impl; + + //close sockets + for(p = 0; p < data->sockets; p++){ + close(data->socket[p].fd); //FIXME does python do this on its own? + Py_XDECREF(data->socket[p].socket); + Py_XDECREF(data->socket[p].handler); + } + + //release interval references + for(p = 0; p <intervals; p++){ + Py_XDECREF(interval[p].reference); + } + Py_XDECREF(data->handler); + + DBGPF("Shutting down interpreter for instance %s", inst[u]->name); + //swap to interpreter and end it, GIL is held after this but state is NULL + PyThreadState_Swap(data->interpreter); + PyErr_Clear(); + //PyThreadState_Clear(data->interpreter); + Py_EndInterpreter(data->interpreter); + + free(data); + } + + //shut down main interpreter + PyThreadState_Swap(python_main); + if(Py_FinalizeEx()){ + LOG("Failed to destroy python interpreters"); + } + PyMem_RawFree(program_name); + } + + LOG("Backend shut down"); + return 0; +} diff --git a/backends/python.h b/backends/python.h new file mode 100644 index 0000000..020aeac --- /dev/null +++ b/backends/python.h @@ -0,0 +1,48 @@ +#include "midimonster.h" + +MM_PLUGIN_API int init(); +static uint32_t python_interval(); +static int python_configure(char* option, char* value); +static int python_configure_instance(instance* inst, char* option, char* value); +static int python_instance(instance* inst); +static channel* python_channel(instance* inst, char* spec, uint8_t flags); +static int python_set(instance* inst, size_t num, channel** c, channel_value* v); +static int python_handle(size_t num, managed_fd* fds); +static int python_start(size_t n, instance** inst); +static int python_shutdown(size_t n, instance** inst); + +typedef struct /*_python_channel_data*/ { + char* name; + PyObject* handler; + double in; + double out; + uint8_t mark; +} mmpython_channel; + +typedef struct /*_mmpy_registered_socket*/ { + int fd; + PyObject* handler; + PyObject* socket; +} mmpy_socket; + +typedef struct /*_mmpy_interval*/ { + uint64_t interval; + uint64_t delta; + PyObject* reference; + PyThreadState* interpreter; +} mmpy_timer; + +typedef struct /*_python_instance_data*/ { + PyThreadState* interpreter; + PyObject* config; //TODO + + size_t sockets; + mmpy_socket* socket; + + size_t channels; + mmpython_channel* channel; + mmpython_channel* current_channel; + + char* default_handler; + PyObject* handler; +} python_instance_data; diff --git a/backends/python.md b/backends/python.md new file mode 100644 index 0000000..6852a79 --- /dev/null +++ b/backends/python.md @@ -0,0 +1,101 @@ +### The `python` backend + +The `python` backend provides a flexible programming environment, allowing users +to route, generate and manipulate channel events using the Python 3 scripting language. + +Every instance has its own interpreter, which can be loaded with multiple Python modules. +These modules may contain member functions accepting a single `float` parameter, which can +then be used as target channels. For each incoming event, the handler function is called. +Channels in the global scope may be assigned a default handler function. + +Python modules may also register `socket` objects (and an associated callback function) with +the MIDIMonster core, which will then alert the module when there is data ready to be read. + +To interact with the MIDIMonster core, import the `midimonster` module from within your module. + +The `midimonster` module provides the following functions: + +| Function | Usage example | Description | +|-------------------------------|---------------------------------------|-----------------------------------------------| +| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel | +| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel | +| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel | +| `current()` | `print(midimonster.current())` | Returns the name of the input channel whose handler function is currently running or `None` if the interpreter was called from another context | +| `timestamp()` | `print(midimonster.timestamp())` | Get the internal core timestamp (in milliseconds) | +| `interval(function, long)` | `midimonster.interval(toggle, 100)` | Register a function to be called periodically. Interval is specified in milliseconds (accurate to 10msec). Calling `interval` with the same function again updates the interval. Specifying the interval as `0` cancels the interval | +| `manage(function, socket)` | `midimonster.manage(handler, socket)`| Register a (connected/listening) socket to the MIDIMonster core. Calls `function(socket)` when the socket is ready to read. Calling this method with `None` as the function argument unregisters the socket. A socket may only have one associated handler | + +Example Python module: +```python +import socket +import midimonster + +# Simple channel handler +def in1(value): + midimonster.output("out1", 1 - value) + +# Socket data handler +def socket_handler(sock): + # This should get some more error handling + data = sock.recv(1024) + print("Received %d bytes from socket: %s" % (len(data), data)) + if(len(data) == 0): + # Unmanage the socket if it has been closed + midimonster.manage(None, sock) + sock.close() + +# Interval handler +def ping(): + print(midimonster.timestamp()) + +# Register an interval +midimonster.interval(ping, 1000) +# Create and register a client socket (add error handling as you like) +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.connect(("localhost", 8990)) +midimonster.manage(socket_handler, s) +``` + +Input values range between 0.0 and 1.0, output values are clamped to the same range. + +Note that registered sockets that have been closed (`socket.recv()` returned 0 bytes) +need to be unregistered from the MIDIMonster core, otherwise the core socket multiplexing +mechanism will report an error and shut down the MIDIMonster. + +#### Global configuration + +The `python` backend does not take any global configuration. + +#### Instance configuration + +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------------------------------| +| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location | +| `default-handler` | `mu_handlers.default` | none | Function to be called as handler for all top-level channels (not belonging to a module) | + +A single instance may have multiple `module` options specified. This will make all handlers available within their +module namespaces (see the section on channel specification). + +#### Channel specification + +Channel names may be any valid Python function name. To call handler functions in a module, +specify the channel as the functions qualified path (by prefixing it with the module name and a dot). + +Example mappings: +``` +py1.my_handlers.in1 < py1.foo +py1.out1 > py2.module.handler +``` + +#### Known bugs / problems + +Output values will not trigger corresponding input event handlers unless the channel is mapped +back in the MIDIMonster configuration. This is intentional. + +Importing a Python module named `midimonster` is probably a bad idea and thus unsupported. + +The MIDIMonster is, at its core, single-threaded. Do not try to use Python's `threading` +module with the MIDIMonster. + +Note that executing Python code blocks the MIDIMonster core. It is not a good idea to call functions that +take a long time to complete (such as `time.sleep()`) within your Python modules. diff --git a/backends/sacn.c b/backends/sacn.c index ff2b61e..bd5c75a 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -28,12 +28,14 @@ static struct /*_sacn_global_config*/ { size_t fds; sacn_fd* fd; uint64_t last_announce; + uint32_t next_frame; } global_cfg = { .source_name = "MIDIMonster", .cid = {'M', 'I', 'D', 'I', 'M', 'o', 'n', 's', 't', 'e', 'r'}, .fds = 0, .fd = NULL, - .last_announce = 0 + .last_announce = 0, + .next_frame = 0 }; MM_PLUGIN_API int init(){ @@ -46,6 +48,7 @@ MM_PLUGIN_API int init(){ .handle = sacn_set, .process = sacn_handle, .start = sacn_start, + .interval = sacn_interval, .shutdown = sacn_shutdown }; @@ -63,6 +66,13 @@ MM_PLUGIN_API int init(){ return 0; } +static uint32_t sacn_interval(){ + if(global_cfg.next_frame){ + return global_cfg.next_frame; + } + return SACN_KEEPALIVE_INTERVAL; +} + static int sacn_listener(char* host, char* port, uint8_t flags){ int fd = -1, yes = 1; if(global_cfg.fds >= MAX_FDS){ @@ -87,7 +97,6 @@ static int sacn_listener(char* host, char* port, uint8_t flags){ global_cfg.fd[global_cfg.fds].fd = fd; global_cfg.fd[global_cfg.fds].universes = 0; global_cfg.fd[global_cfg.fds].universe = NULL; - global_cfg.fd[global_cfg.fds].last_frame = NULL; if(flags & mcast_loop){ //set IP_MCAST_LOOP to allow local applications to receive output @@ -190,24 +199,31 @@ static int sacn_configure_instance(instance* inst, char* option, char* value){ data->unicast_input = strtoul(value, NULL, 10); return 0; } + else if(!strcmp(option, "realtime")){ + data->realtime = strtoul(value, NULL, 10); + return 0; + } LOGPF("Unknown instance configuration option %s for instance %s", option, inst->name); return 1; } -static instance* sacn_instance(){ - instance* inst = mm_instance(); - if(!inst){ - return NULL; - } +static int sacn_instance(instance* inst){ + sacn_instance_data* data = calloc(1, sizeof(sacn_instance_data)); + size_t u; - inst->impl = calloc(1, sizeof(sacn_instance_data)); - if(!inst->impl){ + if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; + } + + for(u = 0; u < sizeof(data->data.channel) / sizeof(channel); u++){ + data->data.channel[u].ident = u; + data->data.channel[u].instance = inst; } - return inst; + inst->impl = data; + return 0; } static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ @@ -215,7 +231,7 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ char* spec_next = spec; unsigned chan_a = strtoul(spec, &spec_next, 10), chan_b = 0; - + //range check if(!chan_a || chan_a > 512){ LOGPF("Channel out of range on instance %s: %s", inst->name, spec); @@ -223,6 +239,11 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ } chan_a--; + //check output capabilities + if((flags & mmchannel_output) && !data->xmit_prio){ + LOGPF("Channel %s.%s mapped for output, but instance is not configured for output (no priority set)", inst->name, spec); + } + //if wide channel, mark fine if(*spec_next == '+'){ chan_b = strtoul(spec_next + 1, NULL, 10); @@ -251,7 +272,7 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ } data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a); - return mm_channel(inst, chan_a, 1); + return data->data.channel + chan_a; } static int sacn_transmit(instance* inst){ @@ -294,10 +315,11 @@ static int sacn_transmit(instance* inst){ LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); } - //update last transmit timestamp + //update last transmit timestamp, unmark instance for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){ - if(global_cfg.fd[data->fd_index].universe[u] == data->uni){ - global_cfg.fd[data->fd_index].last_frame[u] = mm_timestamp(); + if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){ + global_cfg.fd[data->fd_index].universe[u].last_frame = mm_timestamp(); + global_cfg.fd[data->fd_index].universe[u].mark = 0; } } return 0; @@ -305,6 +327,7 @@ static int sacn_transmit(instance* inst){ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t u, mark = 0; + uint32_t frame_delta = 0; sacn_instance_data* data = (sacn_instance_data*) inst->impl; if(!num){ @@ -338,6 +361,25 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ //send packet if required if(mark){ + if(!data->realtime){ + //find output instance data + for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){ + if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){ + break; + } + } + + frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].universe[u].last_frame; + + //check if ratelimiting engaged + if(frame_delta < SACN_FRAME_TIMEOUT){ + global_cfg.fd[data->fd_index].universe[u].mark = 1; + if(!global_cfg.next_frame || global_cfg.next_frame > (SACN_FRAME_TIMEOUT - frame_delta)){ + global_cfg.next_frame = (SACN_FRAME_TIMEOUT - frame_delta); + } + return 0; + } + } sacn_transmit(inst); } @@ -389,16 +431,9 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame if(inst_data->data.map[u] & MAP_MARK){ //unmark and get channel inst_data->data.map[u] &= ~MAP_MARK; + chan = inst_data->data.channel + u; if(inst_data->data.map[u] & MAP_FINE){ - chan = mm_channel(inst, MAPPED_CHANNEL(inst_data->data.map[u]), 0); - } - else{ - chan = mm_channel(inst, u, 0); - } - - if(!chan){ - LOGPF("Active channel %" PRIsize_t " on %s not known to core", u, inst->name); - return 1; + chan = inst_data->data.channel + MAPPED_CHANNEL(inst_data->data.map[u]); } //generate value @@ -473,6 +508,7 @@ static void sacn_discovery(size_t fd){ static int sacn_handle(size_t num, managed_fd* fds){ size_t u, c; uint64_t timestamp = mm_timestamp(); + uint32_t synthesize_delta = 0; ssize_t bytes_read; char recv_buf[SACN_RECV_BUF]; instance* inst = NULL; @@ -482,7 +518,7 @@ static int sacn_handle(size_t num, managed_fd* fds){ sacn_frame_root* frame = (sacn_frame_root*) recv_buf; sacn_frame_data* data = (sacn_frame_data*) (recv_buf + sizeof(sacn_frame_root)); - if(mm_timestamp() - global_cfg.last_announce > SACN_DISCOVERY_TIMEOUT){ + if(timestamp - global_cfg.last_announce > SACN_DISCOVERY_TIMEOUT){ //send universe discovery pdu for(u = 0; u < global_cfg.fds; u++){ if(global_cfg.fd[u].universes){ @@ -492,17 +528,29 @@ static int sacn_handle(size_t num, managed_fd* fds){ global_cfg.last_announce = timestamp; } - //check for keepalive frames + //check for keepalive frames, synthesize frames if necessary + global_cfg.next_frame = 0; for(u = 0; u < global_cfg.fds; u++){ for(c = 0; c < global_cfg.fd[u].universes; c++){ - if(timestamp - global_cfg.fd[u].last_frame[c] >= SACN_KEEPALIVE_INTERVAL){ + synthesize_delta = timestamp - global_cfg.fd[u].universe[c].last_frame; + + if((global_cfg.fd[u].universe[c].mark + && synthesize_delta >= SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN) + || synthesize_delta >= SACN_KEEPALIVE_INTERVAL){ instance_id.fields.fd_index = u; - instance_id.fields.uni = global_cfg.fd[u].universe[c]; + instance_id.fields.uni = global_cfg.fd[u].universe[c].universe; inst = mm_instance_find(BACKEND_NAME, instance_id.label); if(inst){ sacn_transmit(inst); } } + + //update next frame request + if(global_cfg.fd[u].universe[c].mark + && (!global_cfg.next_frame || global_cfg.next_frame > SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN - synthesize_delta)){ + global_cfg.next_frame = SACN_FRAME_TIMEOUT + SACN_SYNTHESIZE_MARGIN - synthesize_delta; + } + } } @@ -562,7 +610,6 @@ static int sacn_start(size_t n, instance** inst){ if(!global_cfg.fds){ LOG("Failed to start, no descriptors bound"); - free(inst); return 1; } @@ -595,13 +642,15 @@ static int sacn_start(size_t n, instance** inst){ if(data->xmit_prio){ //add to list of advertised universes for this fd - global_cfg.fd[data->fd_index].universe = realloc(global_cfg.fd[data->fd_index].universe, (global_cfg.fd[data->fd_index].universes + 1) * sizeof(uint16_t)); + global_cfg.fd[data->fd_index].universe = realloc(global_cfg.fd[data->fd_index].universe, (global_cfg.fd[data->fd_index].universes + 1) * sizeof(sacn_output_universe)); if(!global_cfg.fd[data->fd_index].universe){ LOG("Failed to allocate memory"); goto bail; } - global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes] = data->uni; + global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes].universe = data->uni; + global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes].last_frame = 0; + global_cfg.fd[data->fd_index].universe[global_cfg.fd[data->fd_index].universes].mark = 0; global_cfg.fd[data->fd_index].universes++; //generate multicast destination address if none set @@ -617,12 +666,6 @@ static int sacn_start(size_t n, instance** inst){ LOGPF("Registering %" PRIsize_t " descriptors to core", 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)); - if(!global_cfg.fd[u].last_frame){ - LOG("Failed to allocate memory"); - goto bail; - } if(mm_manage_fd(global_cfg.fd[u].fd, BACKEND_NAME, 1, (void*) u)){ goto bail; } @@ -643,7 +686,6 @@ static int sacn_shutdown(size_t n, instance** inst){ for(p = 0; p < global_cfg.fds; p++){ close(global_cfg.fd[p].fd); free(global_cfg.fd[p].universe); - free(global_cfg.fd[p].last_frame); } free(global_cfg.fd); LOG("Backend shut down"); diff --git a/backends/sacn.h b/backends/sacn.h index c8d11e9..4138f45 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -1,9 +1,10 @@ #include "midimonster.h" MM_PLUGIN_API int init(); +static uint32_t sacn_interval(); static int sacn_configure(char* option, char* value); static int sacn_configure_instance(instance* instance, char* option, char* value); -static instance* sacn_instance(); +static int sacn_instance(instance* inst); 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); @@ -12,7 +13,11 @@ static int sacn_shutdown(size_t n, instance** inst); #define SACN_PORT "5568" #define SACN_RECV_BUF 8192 -#define SACN_KEEPALIVE_INTERVAL 2000 +//spec 6.6.2.1 +#define SACN_KEEPALIVE_INTERVAL 1000 +//spec 6.6.1 +#define SACN_FRAME_TIMEOUT 20 +#define SACN_SYNTHESIZE_MARGIN 10 #define SACN_DISCOVERY_TIMEOUT 9000 #define SACN_PDU_MAGIC "ASC-E1.17\0\0\0" @@ -31,10 +36,12 @@ typedef struct /*_sacn_universe_model*/ { uint8_t in[512]; uint8_t out[512]; uint16_t map[512]; + channel channel[512]; } sacn_universe; typedef struct /*_sacn_instance_model*/ { uint16_t uni; + uint8_t realtime; uint8_t xmit_prio; uint8_t cid_filter[16]; uint8_t filter_enabled; @@ -54,11 +61,16 @@ typedef union /*_sacn_instance_id*/ { uint64_t label; } sacn_instance_id; +typedef struct /*_sacn_output_universe*/ { + uint16_t universe; + uint64_t last_frame; + uint8_t mark; +} sacn_output_universe; + typedef struct /*_sacn_socket*/ { int fd; size_t universes; - uint16_t* universe; - uint64_t* last_frame; + sacn_output_universe* universe; } sacn_fd; #pragma pack(push, 1) diff --git a/backends/sacn.md b/backends/sacn.md index f5f1db4..598f430 100644 --- a/backends/sacn.md +++ b/backends/sacn.md @@ -26,6 +26,7 @@ This has the side effect of mirroring the output of instances on those descripto | `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 | +| `realtime` | `1` | `0` | Disable the recommended rate-limiting (approx. 44 packets per second) for this instance | Note that instances accepting multicast input also process unicast frames directed at them, while instances in `unicast` mode will not receive multicast frames. @@ -50,9 +51,6 @@ A normal channel that is part of a wide channel can not be mapped individually. 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. diff --git a/backends/winmidi.c b/backends/winmidi.c index 0722ca2..d9b3047 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -95,19 +95,14 @@ static int winmidi_configure_instance(instance* inst, char* option, char* value) 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){ +static int winmidi_instance(instance* inst){ + inst->impl = calloc(1, sizeof(winmidi_instance_data)); + if(!inst->impl){ LOG("Failed to allocate memory"); - return NULL; + return 1; } - return i; + return 0; } static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ @@ -122,7 +117,7 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ next_token = spec + 7; } } - + if(!next_token){ LOGPF("Invalid channel specification %s", spec); return NULL; @@ -218,7 +213,7 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v midiOutShortMsg(data->device_out, output.dword); } - + return 0; } @@ -263,7 +258,7 @@ static int winmidi_handle(size_t num, managed_fd* fds){ 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); + backend_config.event[u].value.normalised); } else{ LOGPF("Incoming data on channel %s.ch%d.%s%d, value %f", @@ -271,7 +266,7 @@ static int winmidi_handle(size_t num, managed_fd* fds){ 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); + backend_config.event[u].value.normalised); } } chan = mm_channel(backend_config.event[u].inst, backend_config.event[u].channel.label, 0); @@ -315,7 +310,7 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW ident.fields.type = input.components.status & 0xF0; ident.fields.control = input.components.data1; val.normalised = (double) input.components.data2 / 127.0; - + if(ident.fields.type == 0x80){ ident.fields.type = note; val.normalised = 0; @@ -340,7 +335,6 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW case MIM_CLOSE: //device opened/closed return; - } DBGPF("Incoming message type %d channel %d control %d value %f", @@ -396,7 +390,7 @@ static int winmidi_match_input(char* prefix){ for(n = 0; n < inputs; n++){ midiInGetDevCaps(n, &input_caps, sizeof(MIDIINCAPS)); if(!prefix){ - printf("\tID %d: %s", n, input_caps.szPname); + LOGPF("\tID %d: %s", n, input_caps.szPname); } else if(!strncmp(input_caps.szPname, prefix, strlen(prefix))){ LOGPF("Selected input device %s (ID %" PRIsize_t ") for name %s", input_caps.szPname, n, prefix); @@ -429,7 +423,7 @@ static int winmidi_match_output(char* prefix){ for(n = 0; n < outputs; n++){ midiOutGetDevCaps(n, &output_caps, sizeof(MIDIOUTCAPS)); if(!prefix){ - printf("\tID %d: %s", n, output_caps.szPname); + LOGPF("\tID %d: %s", n, output_caps.szPname); } else if(!strncmp(output_caps.szPname, prefix, strlen(prefix))){ LOGPF("Selected output device %s (ID %" PRIsize_t " for name %s", output_caps.szPname, n, prefix); @@ -440,32 +434,22 @@ static int winmidi_match_output(char* prefix){ return -1; } -static int winmidi_start(size_t n, instance** inst){ - size_t p; - int device, rv = -1; - winmidi_instance_data* data = NULL; +static int winmidi_socket_pair(int* fds){ + //this really should be a size_t but getsockname specifies int* for some reason + int sockadd_len = sizeof(struct sockaddr_storage); + char* error = NULL; struct sockaddr_storage sockadd = { 0 }; - //this really should be a size_t but getsockname specifies int* for some reason - int sockadd_len = sizeof(sockadd); - char* error = NULL; - DBGPF("Main thread ID is %ld", GetCurrentThreadId()); - //output device list if requested - if(backend_config.list_devices){ - winmidi_match_input(NULL); - winmidi_match_output(NULL); - } - - //open the feedback sockets //for some reason the feedback connection fails to work on 'real' windows with ipv6 - backend_config.socket_pair[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); - if(backend_config.socket_pair[0] < 0){ + fds[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); + if(fds[0] < 0){ LOG("Failed to open feedback socket"); return 1; } - if(getsockname(backend_config.socket_pair[0], (struct sockaddr*) &sockadd, &sockadd_len)){ + + if(getsockname(fds[0], (struct sockaddr*) &sockadd, &sockadd_len)){ FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); LOGPF("Failed to query feedback socket information: %s", error); @@ -488,8 +472,8 @@ static int winmidi_start(size_t n, instance** inst){ return 1; } DBGPF("Feedback socket family %d port %d", sockadd.ss_family, be16toh(((struct sockaddr_in*)&sockadd)->sin_port)); - backend_config.socket_pair[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP); - if(backend_config.socket_pair[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){ + fds[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP); + if(fds[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){ FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); LOGPF("Failed to connect to feedback socket: %s", error); @@ -497,6 +481,26 @@ static int winmidi_start(size_t n, instance** inst){ return 1; } + return 0; +} + +static int winmidi_start(size_t n, instance** inst){ + size_t p; + int device, rv = -1; + winmidi_instance_data* data = NULL; + DBGPF("Main thread ID is %ld", GetCurrentThreadId()); + + //output device list if requested + if(backend_config.list_devices){ + winmidi_match_input(NULL); + winmidi_match_output(NULL); + } + + //open the feedback sockets + if(winmidi_socket_pair(backend_config.socket_pair)){ + return 1; + } + //set up instances and start input for(p = 0; p < n; p++){ data = (winmidi_instance_data*) inst[p]->impl; diff --git a/backends/winmidi.h b/backends/winmidi.h index 81e7439..4c740ea 100644 --- a/backends/winmidi.h +++ b/backends/winmidi.h @@ -3,7 +3,7 @@ MM_PLUGIN_API int init(); static int winmidi_configure(char* option, char* value); static int winmidi_configure_instance(instance* inst, char* option, char* value); -static instance* winmidi_instance(); +static int winmidi_instance(instance* inst); 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); @@ -21,6 +21,8 @@ typedef enum { static backend* current_backend = NULL; static instance* current_instance = NULL; +static size_t noverrides = 0; +static config_override* overrides = NULL; #ifdef _WIN32 #define GETLINE_BUFFER 4096 @@ -277,7 +279,7 @@ static int config_map(char* to_raw, char* from_raw){ || 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){ @@ -296,7 +298,7 @@ static int config_map(char* to_raw, char* from_raw){ 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; @@ -312,13 +314,183 @@ done: return rv; } +static int config_line(char* line){ + map_type mapping_type = map_rtl; + char* separator = NULL; + size_t u; + + line = config_trim_line(line); + if(*line == ';' || strlen(line) == 0){ + //skip comments + return 0; + } + if(*line == '[' && line[strlen(line) - 1] == ']'){ + if(!strncmp(line, "[backend ", 9)){ + //backend configuration + parser_state = backend_cfg; + line[strlen(line) - 1] = 0; + current_backend = backend_match(line + 9); + + if(!current_backend){ + fprintf(stderr, "Cannot configure unknown backend %s\n", line + 9); + return 1; + } + + //apply overrides + for(u = 0; u < noverrides; u++){ + if(!overrides[u].handled && overrides[u].type == override_backend + && !strcmp(overrides[u].target, current_backend->name)){ + if(current_backend->conf(overrides[u].option, overrides[u].value)){ + fprintf(stderr, "Configuration override for %s failed for backend %s\n", + overrides[u].option, current_backend->name); + return 1; + } + overrides[u].handled = 1; + } + } + } + else if(!strcmp(line, "[map]")){ + //mapping configuration + parser_state = map; + } + else{ + //backend instance configuration + parser_state = instance_cfg; + + //trim braces + line[strlen(line) - 1] = 0; + line++; + + //find separating space and terminate + for(separator = line; *separator && *separator != ' '; separator++){ + } + if(!*separator){ + fprintf(stderr, "No instance name specified for backend %s\n", line); + return 1; + } + *separator = 0; + separator++; + + current_backend = backend_match(line); + if(!current_backend){ + fprintf(stderr, "No such backend %s\n", line); + return 1; + } + + if(instance_match(separator)){ + fprintf(stderr, "Duplicate instance name %s\n", separator); + return 1; + } + + //validate instance name + if(strchr(separator, ' ') || strchr(separator, '.')){ + fprintf(stderr, "Invalid instance name %s\n", separator); + return 1; + } + + current_instance = mm_instance(current_backend); + if(!current_instance){ + return 1; + } + + if(current_backend->create(current_instance)){ + fprintf(stderr, "Failed to create %s instance %s\n", line, separator); + return 1; + } + + current_instance->name = strdup(separator); + current_instance->backend = current_backend; + fprintf(stderr, "Created %s instance %s\n", line, separator); + + //apply overrides + for(u = 0; u < noverrides; u++){ + if(!overrides[u].handled && overrides[u].type == override_instance + && !strcmp(overrides[u].target, current_instance->name)){ + if(current_backend->conf_instance(current_instance, overrides[u].option, overrides[u].value)){ + fprintf(stderr, "Configuration override for %s failed for instance %s\n", + overrides[u].option, current_instance->name); + return 1; + } + overrides[u].handled = 1; + } + } + } + } + else if(parser_state == map){ + mapping_type = map_rtl; + //find separator + for(separator = line; *separator && *separator != '<' && *separator != '>'; separator++){ + } + + switch(*separator){ + case '>': + mapping_type = map_ltr; + //fall through + case '<': //default + *separator = 0; + separator++; + break; + case 0: + default: + fprintf(stderr, "Not a channel mapping: %s\n", line); + return 1; + } + + if((mapping_type == map_ltr && *separator == '<') + || (mapping_type == map_rtl && *separator == '>')){ + mapping_type = map_bidir; + separator++; + } + + line = config_trim_line(line); + separator = config_trim_line(separator); + + if(mapping_type == map_ltr || mapping_type == map_bidir){ + if(config_map(separator, line)){ + fprintf(stderr, "Failed to map channel %s to %s\n", line, separator); + return 1; + } + } + if(mapping_type == map_rtl || mapping_type == map_bidir){ + if(config_map(line, separator)){ + fprintf(stderr, "Failed to map channel %s to %s\n", separator, line); + return 1; + } + } + } + else{ + //pass to parser + //find separator + separator = strchr(line, '='); + if(!separator){ + fprintf(stderr, "Not an assignment: %s\n", line); + return 1; + } + + *separator = 0; + separator++; + line = config_trim_line(line); + separator = config_trim_line(separator); + + if(parser_state == backend_cfg && current_backend->conf(line, separator)){ + fprintf(stderr, "Failed to configure backend %s\n", current_backend->name); + return 1; + } + else if(parser_state == instance_cfg && current_backend->conf_instance(current_instance, line, separator)){ + fprintf(stderr, "Failed to configure instance %s\n", current_instance->name); + return 1; + } + } + + return 0; +} + int config_read(char* cfg_filepath){ int rv = 1; size_t line_alloc = 0; ssize_t status; - map_type mapping_type = map_rtl; FILE* source = NULL; - char* line_raw = NULL, *line, *separator; + char* line_raw = NULL; //create heap copy of file name because original might be in readonly memory char* source_dir = strdup(cfg_filepath), *source_file = NULL; @@ -355,146 +527,88 @@ int config_read(char* cfg_filepath){ } for(status = getline(&line_raw, &line_alloc, source); status >= 0; status = getline(&line_raw, &line_alloc, source)){ - line = config_trim_line(line_raw); - if(*line == ';' || strlen(line) == 0){ - //skip comments - continue; + if(config_line(line_raw)){ + goto bail; } - if(*line == '[' && line[strlen(line) - 1] == ']'){ - if(!strncmp(line, "[backend ", 9)){ - //backend configuration - parser_state = backend_cfg; - line[strlen(line) - 1] = 0; - current_backend = backend_match(line + 9); - - if(!current_backend){ - fprintf(stderr, "Cannot configure unknown backend %s\n", line + 9); - goto bail; - } - } - else if(!strcmp(line, "[map]")){ - //mapping configuration - parser_state = map; - } - else{ - //backend instance configuration - parser_state = instance_cfg; - - //trim braces - line[strlen(line) - 1] = 0; - line++; - - //find separating space and terminate - for(separator = line; *separator && *separator != ' '; separator++){ - } - if(!*separator){ - fprintf(stderr, "No instance name specified for backend %s\n", line); - goto bail; - } - *separator = 0; - separator++; + } - current_backend = backend_match(line); - if(!current_backend){ - fprintf(stderr, "No such backend %s\n", line); - goto bail; - } + //TODO check whether all overrides have been applied - if(instance_match(separator)){ - fprintf(stderr, "Duplicate instance name %s\n", separator); - goto bail; - } + rv = 0; +bail: + free(source_dir); + if(source){ + fclose(source); + } + free(line_raw); + return rv; +} - //validate instance name - if(strchr(separator, ' ') || strchr(separator, '.')){ - fprintf(stderr, "Invalid instance name %s\n", separator); - goto bail; - } +int config_add_override(override_type type, char* data_raw){ + int rv = 1; + //heap a copy because the original data is probably not writable + char* data = strdup(data_raw); - current_instance = current_backend->create(); - if(!current_instance){ - fprintf(stderr, "Failed to instantiate backend %s\n", line); - goto bail; - } + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + goto bail; + } - current_instance->name = strdup(separator); - current_instance->backend = current_backend; - fprintf(stderr, "Created %s instance %s\n", line, separator); - } - } - else if(parser_state == map){ - mapping_type = map_rtl; - //find separator - for(separator = line; *separator && *separator != '<' && *separator != '>'; separator++){ - } + char* option = strchr(data, '.'); + char* value = strchr(data, '='); - switch(*separator){ - case '>': - mapping_type = map_ltr; - //fall through - case '<': //default - *separator = 0; - separator++; - break; - case 0: - default: - fprintf(stderr, "Not a channel mapping: %s\n", line); - goto bail; - } + if(!option || !value){ + fprintf(stderr, "Override %s is not a valid assignment\n", data_raw); + goto bail; + } - if((mapping_type == map_ltr && *separator == '<') - || (mapping_type == map_rtl && *separator == '>')){ - mapping_type = map_bidir; - separator++; - } + //terminate strings + *option = 0; + option++; - line = config_trim_line(line); - separator = config_trim_line(separator); + *value = 0; + value++; - if(mapping_type == map_ltr || mapping_type == map_bidir){ - if(config_map(separator, line)){ - fprintf(stderr, "Failed to map channel %s to %s\n", line, separator); - goto bail; - } - } - if(mapping_type == map_rtl || mapping_type == map_bidir){ - if(config_map(line, separator)){ - fprintf(stderr, "Failed to map channel %s to %s\n", separator, line); - goto bail; - } - } - } - else{ - //pass to parser - //find separator - separator = strchr(line, '='); - if(!separator){ - fprintf(stderr, "Not an assignment: %s\n", line); - goto bail; - } + config_override new = { + .type = type, + .handled = 0, + .target = strdup(config_trim_line(data)), + .option = strdup(config_trim_line(option)), + .value = strdup(config_trim_line(value)) + }; - *separator = 0; - separator++; - line = config_trim_line(line); - separator = config_trim_line(separator); + if(!new.target || !new.option || !new.value){ + fprintf(stderr, "Failed to allocate memory\n"); + goto bail; + } - if(parser_state == backend_cfg && current_backend->conf(line, separator)){ - fprintf(stderr, "Failed to configure backend %s\n", current_backend->name); - goto bail; - } - else if(parser_state == instance_cfg && current_backend->conf_instance(current_instance, line, separator)){ - fprintf(stderr, "Failed to configure instance %s\n", current_instance->name); - goto bail; - } - } + overrides = realloc(overrides, (noverrides + 1) * sizeof(config_override)); + if(!overrides){ + noverrides = 0; + fprintf(stderr, "Failed to allocate memory\n"); + goto bail; } + overrides[noverrides] = new; + noverrides++; rv = 0; bail: - free(source_dir); - if(source){ - fclose(source); - } - free(line_raw); + free(data); return rv; } + +void config_free(){ + size_t u; + + for(u = 0; u < noverrides; u++){ + free(overrides[u].target); + free(overrides[u].option); + free(overrides[u].value); + } + + noverrides = 0; + free(overrides); + overrides = NULL; + + parser_state = none; +} @@ -1 +1,45 @@ +/* + * 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; + +/* + * Command-line override types + */ +typedef enum { + override_backend, + override_instance +} override_type; + +/* + * Command-line override data + */ +typedef struct /*_mm_config_override*/ { + override_type type; + uint8_t handled; + char* target; + char* option; + char* value; +} config_override; + int config_read(char* file); +int config_add_override(override_type type, char* data); +void config_free(); diff --git a/configs/launchctl-sacn.cfg b/configs/launchctl-sacn.cfg index dedfc0f..10a736a 100644 --- a/configs/launchctl-sacn.cfg +++ b/configs/launchctl-sacn.cfg @@ -7,7 +7,7 @@ name = MIDIMonster [backend sacn] -bind = 0.0.0.0 +bind = 0.0.0.0 5568 local [midi lc] read = Launch Control diff --git a/configs/layering.cfg b/configs/layering.cfg new file mode 100644 index 0000000..7adcd6f --- /dev/null +++ b/configs/layering.cfg @@ -0,0 +1,23 @@ +; This configuration demonstrates how to create a "layered" mapping +; using the Lua backend. The 'control' channel on the layers instance +; selects the offset to which the 16 input channels (mapped from +; the rotaries of a Launch Control) are mapped on the output instance + +[backend artnet] +bind = 0.0.0.0 + +[midi in] +read = Launch Control + +[artnet out] +destination = 255.255.255.255 +universe = 1 + +[lua layers] +script = layering.lua + +[map] +in.ch0.cc{0..15} > layers.in{0..15} +layers.out{0..511} > out.{1..512} + +in.ch0.note0 > layers.control diff --git a/configs/layering.lua b/configs/layering.lua new file mode 100644 index 0000000..5d9458d --- /dev/null +++ b/configs/layering.lua @@ -0,0 +1,76 @@ +-- This global variable has the current base offset for the input channels. +-- We want to map 16 input channels (from MIDI) to 512 output channels (ArtNet), +-- so we have 32 possible offsets (32 * 16 = 512) +current_layer = 0 + +-- Set the current_layer based on the control input channel +function control(value) + current_layer = math.floor(value * 31.99); +end + +-- Handler functions for the input channels +-- Calculate the channel offset and just output the value the input channel provides +function in0(value) + output("out"..((current_layer * 16)), value) + print("Output on out"..(current_layer * 16)) +end + +function in1(value) + output("out"..((current_layer * 16) + 1), value) +end + +function in2(value) + output("out"..((current_layer * 16) + 2), value) +end + +function in3(value) + output("out"..((current_layer * 16) + 3), value) +end + +function in4(value) + output("out"..((current_layer * 16) + 4), value) +end + +function in5(value) + output("out"..((current_layer * 16) + 5), value) +end + +function in6(value) + output("out"..((current_layer * 16) + 6), value) +end + +function in7(value) + output("out"..((current_layer * 16) + 7), value) +end + +function in8(value) + output("out"..((current_layer * 16) + 8), value) +end + +function in9(value) + output("out"..((current_layer * 16) + 9), value) +end + +function in10(value) + output("out"..((current_layer * 16) + 10), value) +end + +function in11(value) + output("out"..((current_layer * 16) + 11), value) +end + +function in12(value) + output("out"..((current_layer * 16) + 12), value) +end + +function in13(value) + output("out"..((current_layer * 16) + 13), value) +end + +function in14(value) + output("out"..((current_layer * 16) + 14), value) +end + +function in15(value) + output("out"..((current_layer * 16) + 15), value) +end diff --git a/configs/osc-artnet.cfg b/configs/osc-artnet.cfg index ab1d767..35b2111 100644 --- a/configs/osc-artnet.cfg +++ b/configs/osc-artnet.cfg @@ -5,7 +5,7 @@ bind = 0.0.0.0 [osc touch] -bind = * 8000 +bind = 0.0.0.0 8000 dest = learn@8001 [artnet out] diff --git a/installer.sh b/installer.sh index c85b70e..111c783 100755 --- a/installer.sh +++ b/installer.sh @@ -1,213 +1,279 @@ #!/bin/bash ################################################ SETUP ################################################ -deps=(libasound2-dev libevdev-dev liblua5.3-dev libjack-jackd2-dev pkg-config libssl-dev gcc make wget git) -user=$(whoami) # for bypassing user check replace "$(whoami)" with "root". - -tmp_path=$(mktemp -d) # Repo download path -updater_dir=/etc/midimonster-updater # Updater download + config path -updater_file=$updater_dir/updater.conf - -latest_version=$(curl --silent "https://api.github.com/repos/cbdevnet/midimonster/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') - -makeargs=all # Build args - -VAR_DESTDIR="" # Unused -VAR_PREFIX="/usr" -VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" -VAR_DEFAULT_CFG="/etc/midimonster/midimonster.cfg" -VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" - -bold=$(tput bold) -normal=$(tput sgr0) - -################################################ SETUP ################################################ +deps=( + libasound2-dev + libevdev-dev + liblua5.3-dev + libjack-jackd2-dev + pkg-config + libssl-dev + python3-dev + gcc + make + wget + git +) +# Replace this with 'root' to bypass the user check +user="$(whoami)" +# Temporary directory used for repository clone +tmp_path="$(mktemp -d)" +# Installer/updater install directory +updater_dir="/etc/midimonster-updater" + +latest_version="$(curl --silent "https://api.github.com/repos/cbdevnet/midimonster/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')" + +# make invocation arguments +makeargs="all" + +normal="$(tput sgr0)" +dim="$(tput dim)" +bold="$(tput bold)" +uline="$(tput smul)" +c_red="$(tput setaf 1)" +c_green="$(tput setaf 2)" +c_mag="$(tput setaf 5)" + +DEFAULT_PREFIX="/usr" +DEFAULT_PLUGINPATH="/lib/midimonster" +DEFAULT_CFGPATH="/etc/midimonster/midimonster.cfg" +DEFAULT_EXAMPLES="/share/midimonster" ############################################## FUNCTIONS ############################################## - -INSTALL-DEPS () { ##Install deps from array "$deps" - for t in ${deps[@]}; do - if [ $(dpkg-query -W -f='${Status}' $t 2>/dev/null | grep -c "ok installed") -eq 0 ]; then - printf "Installing %s\n" "$t" - apt-get install $t; - printf "Done\n"; - else - printf "%s already installed!\n" "$t" - fi - done - printf "\n" +assign_defaults(){ + VAR_PREFIX="${VAR_PREFIX:-$DEFAULT_PREFIX}" + VAR_PLUGINS="${VAR_PLUGINS:-$VAR_PREFIX$DEFAULT_PLUGINPATH}" + VAR_DEFAULT_CFG="${VAR_DEFAULT_CFG:-$DEFAULT_CFGPATH}" + VAR_EXAMPLE_CFGS="${VAR_EXAMPLE_CFGS:-$VAR_PREFIX$DEFAULT_EXAMPLES}" } -NIGHTLY_CHECK () { - #Asks for nightly version - read -p "Do you want to install the latest development version? (y/n)? " magic - case "$magic" in - y|Y) - printf "OK! You´re a risky person ;D\n" - NIGHTLY=1 +ARGS(){ + for i in "$@"; do + case "$i" in + --prefix=*) + VAR_PREFIX="${i#*=}" ;; - n|N) - printf "That´s OK - installing the latest stable version for you ;-)\n" - NIGHTLY=0 + --plugins=*) + VAR_PLUGINS="${i#*=}" ;; - *) - printf "Invalid input\n" - ERROR + --defcfg=*) + VAR_DEFAULT_CFG="${i#*=}" ;; - esac - - # Roll back to last tag if we're not on a nightly build - if [ "$NIGHTLY" != 1 ]; then - printf "Finding latest stable version...\n" - Iversion=$(git describe --abbrev=0) - printf "Starting Git checkout to %s...\n" "$Iversion" - git checkout -f -q $Iversion - fi + --examples=*) + VAR_EXAMPLE_CFGS="${i#*=}" + ;; + --dev) + NIGHTLY=1 + ;; + -d|--default) + assign_defaults + ;; + -fu|--forceupdate) + UPDATER_FORCE="1" + ;; + --install-updater) + NIGHTLY=1 prepare_repo + install_script + exit 0 + ;; + --install-dependencies) + install_dependencies + exit 0 + ;; + -h|--help|*) + assign_defaults + printf "${bold}Usage:${normal} ${0} ${c_green}[OPTIONS]${normal}" + printf "\n\t${c_green}--prefix${normal} ${c_red}<path>${normal}\t\tSet the installation prefix\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PREFIX" + printf "\n\t${c_green}--plugins${normal} ${c_red}<path>${normal}\tSet the plugin install path\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PLUGINS" + printf "\n\t${c_green}--defcfg${normal} ${c_red}<path>${normal}\t\tSet the default configuration path\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_DEFAULT_CFG" + printf "\n\t${c_green}--examples${normal} ${c_red}<path>${normal}\tSet the path for example configurations\t${c_mag}Default:${normal} ${dim}%s${normal}\n" "$VAR_EXAMPLE_CFGS" + printf "\n\t${c_green}--dev${normal}\t\t\t\tInstall nightly version" + printf "\n\t${c_green}-d, --default${normal}\t\t\tUse default values to install" + printf "\n\t${c_green}-fu, --forceupdate${normal}\t\tForce the updater to update without a version check" + printf "\n\t${c_green}--install-updater${normal}\t\tInstall the updater (Run with midimonster-updater)" + printf "\n\t${c_green}--install-dependencies${normal}\t\tInstall dependencies and exit" + printf "\n\t${c_green}-h, --help${normal}\t\t\tShow this message and exit" + printf "\n\t${uline}${bold}${c_mag}Each argument can be overwritten by another, the last one is used!${normal}\n" + rmdir "$tmp_path" + exit 1 + ;; + esac + shift + done } -INSTALL-PREP () { - ( - printf "Starting download...\n" - git clone https://github.com/cbdevnet/midimonster.git "$tmp_path" # Gets Midimonster - printf "\nInitializing repository...\n" - cd $tmp_path - git init $tmp_path - printf "\n" - ) - NIGHTLY_CHECK - printf "Preparation successful\n\n" - printf "${bold}If you don't know what you're doing, just hit enter 4 times.${normal}\n" - - read -e -i "$VAR_PREFIX" -p "PREFIX (Install root directory): " input # Reads VAR_PREFIX - VAR_PREFIX="${input:-$VAR_PREFIX}" - - read -e -i "$VAR_PLUGINS" -p "PLUGINS (Plugin directory): " input # Reads VAR_PLUGINS - VAR_PLUGINS="${input:-$VAR_PLUGINS}" - - read -e -i "$VAR_DEFAULT_CFG" -p "Default config path: " input # Reads VAR_DEFAULT_CFG - VAR_DEFAULT_CFG="${input:-$VAR_DEFAULT_CFG}" - - read -e -i "$VAR_EXAMPLE_CFGS" -p "Example config directory: " input # Reads VAR_EXAMPLE_CFGS - VAR_EXAMPLE_CFGS="${input:-$VAR_EXAMPLE_CFGS}" - - UPDATER_SAVE - - export PREFIX=$VAR_PREFIX - export PLUGINS=$VAR_PLUGINS - export DEFAULT_CFG=$VAR_DEFAULT_CFG - export DESTDIR=$VAR_DESTDIR - export EXAMPLES=$VAR_EXAMPLE_CFGS +# Install unmatched dependencies +install_dependencies(){ + for dependency in ${deps[@]}; do + if [ "$(dpkg-query -W -f='${Status}' "$dependency" 2>/dev/null | grep -c "ok installed")" -eq 0 ]; then + printf "Installing %s\n" "$dependency" + apt-get install "$dependency" + else + printf "%s already installed!\n" "$dependency" + fi + done + printf "\n" } -UPDATER-PREP () { - ( - printf "Starting download...\n" - git clone https://github.com/cbdevnet/midimonster.git "$tmp_path" # Gets Midimonster - printf "\nInitializing repository...\n" - cd $tmp_path - git init $tmp_path - printf "Sucessfully imported settings from %s\n" "$updater_file" - ) - NIGHTLY_CHECK - printf "Preparation successful\n\n" - - rm -f "$VAR_PREFIX/bin/midimonster" - rm -rf "$VAR_PLUGINS/" - - UPDATER_SAVE - - export PREFIX=$VAR_PREFIX - export PLUGINS=$VAR_PLUGINS - export DEFAULT_CFG=$VAR_DEFAULT_CFG - export DESTDIR=$VAR_DESTDIR - export EXAMPLES=$VAR_EXAMPLE_CFGS +ask_questions(){ + # Only say if necessary + if [ -n "$VAR_PREFIX" ] || [ -n "$VAR_PLUGINS" ] || [ -n "$VAR_DEFAULT_CFG" ] || [ -n "$VAR_EXAMPLE_CFGS" ]; then + printf "${bold}If you don't know what you're doing, just hit enter a few times.${normal}\n\n" + fi + + if [ -z "$VAR_PREFIX" ]; then + read -e -i "$DEFAULT_PREFIX" -p "PREFIX (Install root directory): " input + VAR_PREFIX="${input:-$VAR_PREFIX}" + fi + + if [ -z "$VAR_PLUGINS" ]; then + read -e -i "$VAR_PREFIX$DEFAULT_PLUGINPATH" -p "PLUGINS (Plugin directory): " input + VAR_PLUGINS="${input:-$VAR_PLUGINS}" + fi + + if [ -z "$VAR_DEFAULT_CFG" ]; then + read -e -i "$DEFAULT_CFGPATH" -p "Default config path: " input + VAR_DEFAULT_CFG="${input:-$VAR_DEFAULT_CFG}" + fi + + if [ -z "$VAR_EXAMPLE_CFGS" ]; then + read -e -i "$VAR_PREFIX$DEFAULT_EXAMPLES" -p "Example config directory: " input + VAR_EXAMPLE_CFGS="${input:-$VAR_EXAMPLE_CFGS}" + fi } -UPDATER () { - installed_version="$(midimonster --version)" - #installed_version="MIDIMonster v0.3-40-gafed325" # FOR TESTING ONLY! (or bypassing updater version check) - if [[ "$installed_version" =~ "$latest_version" ]]; then - printf "Newest Version is already installed! ${bold}($installed_version)${normal}\n\n" - ERROR - else - printf "The installed Version ${bold}´$installed_version´${normal} equals not the newest stable version ${bold}´$latest_version´${normal} (Maybe you are running a development version?)\n\n" +# Clone the repository and select the correct version +prepare_repo(){ + printf "Cloning the repository\n" + git clone "https://github.com/cbdevnet/midimonster.git" "$tmp_path" + + # If not set via argument, ask whether to install development build + if [ -z "$NIGHTLY" ]; then + read -p "Do you want to install the latest development version? (y/n)? " magic + case "$magic" in + y|Y) + printf "OK! You´re a risky person ;D\n" + NIGHTLY=1 + ;; + n|N) + printf "That´s OK - installing the latest stable version for you ;-)\n" + NIGHTLY=0 + ;; + *) + printf "${bold}Invalid input -- INSTALLING LATEST STABLE VERSION!${normal}\n" + NIGHTLY=0 + ;; + esac fi - UPDATER-PREP - INSTALL-RUN - DONE + # Roll back to last tag if a stable version was requested + if [ "$NIGHTLY" != 1 ]; then + cd "$tmp_path" + printf "Finding latest stable version...\n" + last_tag=$(git describe --abbrev=0) + printf "Checking out %s...\n" "$last_tag" + git checkout -f -q "$last_tag" + fi } -INSTALL-RUN () { # Build +# Build and install the software +build(){ + # Export variables for make + export PREFIX="$VAR_PREFIX" + export PLUGINS="$VAR_PLUGINS" + export DEFAULT_CFG="$VAR_DEFAULT_CFG" + export EXAMPLES="$VAR_EXAMPLE_CFGS" + cd "$tmp_path" make clean - make $makeargs + make "$makeargs" make install } -UPDATER_SAVE () { # Saves file for the auto updater in this script - rm -rf $updater_dir - printf "Saving updater to %s/updater.sh\n" "$update_dir" +# Save data for the updater +save_config(){ + rm -f "$updater_dir/updater.conf" + mkdir -p "$updater_dir" + printf "Exporting updater config\n" + printf "VAR_PREFIX=%s\nVAR_PLUGINS=%s\nVAR_DEFAULT_CFG=%s\nVAR_DESTDIR=%s\nVAR_EXAMPLE_CFGS=%s\n" "$VAR_PREFIX" "$VAR_PLUGINS" "$VAR_DEFAULT_CFG" "$VAR_DESTDIR" "$VAR_EXAMPLE_CFGS" > "$updater_dir/updater.conf" +} + +# Updates this script using the one from the checked out repo (containing the requested version) +install_script(){ mkdir -p "$updater_dir" - wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh -O $updater_dir/updater.sh - printf "Creating symlink to updater in /usr/bin/midimonster-updater\n" - ln -s "$updater_dir/updater.sh" "/usr/bin/midimonster-updater" + printf "Copying updater to %s/updater.sh\n" "$updater_dir" + cp "$tmp_path/installer.sh" "$updater_dir/updater.sh" chmod +x "$updater_dir/updater.sh" - printf "Exporting updater config to %s\n" "$updater_file" - printf "VAR_PREFIX=%s\nVAR_PLUGINS=%s\nVAR_DEFAULT_CFG=%s\nVAR_DESTDIR=%s\nVAR_EXAMPLE_CFGS=%s\n" "$VAR_PREFIX" "$VAR_PLUGINS" "$VAR_DEFAULT_CFG" "$VAR_DESTDIR" "$VAR_EXAMPLE_CFGS" > $updater_file + printf "Creating symlink /usr/bin/midimonster-updater\n" + ln -s "$updater_dir/updater.sh" "/usr/bin/midimonster-updater" } -ERROR () { - printf "\nAborting...\n" - CLEAN - printf "Exiting...\n" +error_handler(){ + printf "\nAborting\n" exit 1 } -DONE () { - printf "\nDone.\n" - CLEAN - exit 0 -} - -CLEAN () { - printf "\nCleaning...\n" - rm -rf $tmp_path +cleanup(){ + if [ -d "$tmp_path" ]; then + printf "Cleaning up temporary files...\n" + rm -rf "$tmp_path" + fi } -############################################## FUNCTIONS ############################################## - - ################################################ Main ################################################# -trap ERROR SIGINT SIGTERM SIGKILL +trap error_handler SIGINT SIGTERM +trap cleanup EXIT + +# Parse arguments +ARGS "$@" clear -# Check if $user = root! +# Check whether we have the privileges to install stuff if [ "$user" != "root" ]; then - printf "Installer must be run as root\n" - ERROR + printf "The installer/updater requires root privileges to install the midimonster system-wide\n" + exit 1 fi -if [ $(wget -q --spider http://github.com) $? -eq 1 ]; then - printf "You need connection to the internet\n" - ERROR +# Check if we can download the sources +if [ "$(wget -q --spider http://github.com)" ]; then + printf "The installer/updater requires internet connectivity to download the midimonster sources\n" + exit 1 fi -# Check if updater config file exist and import it (overwrites default values!) -if [ -f $updater_file ]; then - printf "Starting updater...\n\n" - . $updater_file +# Check whether the updater needs to run +if [ -f "$updater_dir/updater.conf" ] || [ "$UPDATER_FORCE" = "1" ]; then + if [ -f "$updater_dir/updater.conf" ]; then + . "$updater_dir/updater.conf" + # Parse arguments again to compensate overwrite from source + ARGS "$@" + printf "Imported settings from %s/updater.conf\n" "$updater_dir" + fi - # Check if binary $updater/bin/midimonster exist. If yes start updater else skip. - if [ -x "$VAR_PREFIX/bin/midimonster" ]; then - UPDATER - else - printf "midimonster binary not found, skipping updater.\n" + if [ -n "$UPDATER_FORCE" ]; then + printf "Forcing the updater to start...\n\n" + elif [ -x "$VAR_PREFIX/bin/midimonster" ]; then + installed_version="$(midimonster --version)" + if [[ "$installed_version" =~ "$latest_version" ]]; then + printf "The installed version ${bold}$installed_version${normal} seems to be up to date\nDoing nothing\n\n" + exit 0 + else + printf "The installed version ${bold}$installed_version${normal} does not match the latest version ${bold}$latest_version${normal}\nMaybe you are running a development version?\n\n" + fi fi + + # Run updater steps + prepare_repo + install_script + save_config + build +else + # Run installer steps + install_dependencies + prepare_repo + ask_questions + install_script + save_config + build fi +exit 0 -INSTALL-DEPS -INSTALL-PREP -printf "\n" -INSTALL-RUN -DONE diff --git a/midimonster.1 b/midimonster.1 index 131ed44..44c414e 100644 --- a/midimonster.1 +++ b/midimonster.1 @@ -4,6 +4,12 @@ midimonster \- Multi-protocol translation tool .SH SYNOPSIS .B midimonster .I config-file +.RB [ "-i" +.IR instance.option=value ] +.RB [ "-b" +.IR backend.option=value ] + +.B midimonster -v .SH DESCRIPTION .B MIDIMonster allows the user to translate any channel on one supported protocol into channel(s) @@ -12,7 +18,25 @@ on any other (or the same) supported protocol. .TP .I config-file The configuration file to read. If not specified, a default configuration file is read. + +.TP +.BI "-i " instance.option=value +Supply an additional instance configuration option +.IR option " for " instance "." +Command-line overrides are applied when the instance is first mentioned in the configuration file. + +.TP +.BI "-b " backend.option=value +Supply an additional backend configuration option +.IR option " to " backend "." +Command-line overrides are applied when the backend is first mentioned in the configuration file. + +.B -v +Display version information .SH "SEE ALSO" Online documentation and repository at https://github.com/cbdevnet/midimonster + +For more and in-depth information see the homepage at https://midimonster.net/ +as well as the knowledge base at https://kb.midimonster.net/ .SH AUTHOR Fabian "cbdev" Stumpf <fjs at fabianstumpf dot de> diff --git a/midimonster.c b/midimonster.c index 2ec165b..5109eab 100644 --- a/midimonster.c +++ b/midimonster.c @@ -9,11 +9,13 @@ #else #define MM_API __attribute__((dllexport)) #endif +#define BACKEND_NAME "core" #include "midimonster.h" #include "config.h" #include "backend.h" #include "plugin.h" +/* Core-internal structures */ typedef struct /*_event_collection*/ { size_t alloc; size_t n; @@ -21,25 +23,40 @@ typedef struct /*_event_collection*/ { channel_value* value; } event_collection; -static size_t mappings = 0; -static channel_mapping* map = NULL; +typedef struct /*_mm_channel_mapping*/ { + channel* from; + size_t destinations; + channel** to; +} channel_mapping; + +static struct { + //routing_hash is set up for 256 buckets + size_t entries[256]; + channel_mapping* map[256]; + + event_collection pool[2]; + event_collection* events; +} routing = { + .events = routing.pool +}; + static size_t fds = 0; static managed_fd* fd = NULL; static volatile sig_atomic_t fd_set_dirty = 1; static uint64_t global_timestamp = 0; -static event_collection event_pool[2] = { - {0}, - {0} -}; -static event_collection* primary = event_pool; - volatile static sig_atomic_t shutdown_requested = 0; static void signal_handler(int signum){ shutdown_requested = 1; } +static size_t routing_hash(channel* key){ + uint64_t repr = (uint64_t) key; + //return 8bit hash for 256 buckets, not ideal but it works + return (repr ^ (repr >> 8) ^ (repr >> 16) ^ (repr >> 24) ^ (repr >> 32)) & 0xFF; +} + MM_API uint64_t mm_timestamp(){ return global_timestamp; } @@ -59,53 +76,66 @@ static void update_timestamp(){ } int mm_map_channel(channel* from, channel* to){ - size_t u, m; + size_t u, m, bucket = routing_hash(from); + //find existing source mapping - for(u = 0; u < mappings; u++){ - if(map[u].from == from){ + for(u = 0; u < routing.entries[bucket]; u++){ + if(routing.map[bucket][u].from == from){ break; } } //create new entry - if(u == mappings){ - map = realloc(map, (mappings + 1) * sizeof(channel_mapping)); - if(!map){ + if(u == routing.entries[bucket]){ + routing.map[bucket] = realloc(routing.map[bucket], (routing.entries[bucket] + 1) * sizeof(channel_mapping)); + if(!routing.map[bucket]){ + routing.entries[bucket] = 0; fprintf(stderr, "Failed to allocate memory\n"); return 1; } - memset(map + mappings, 0, sizeof(channel_mapping)); - mappings++; - map[u].from = from; + + memset(routing.map[bucket] + routing.entries[bucket], 0, sizeof(channel_mapping)); + routing.entries[bucket]++; + routing.map[bucket][u].from = from; } //check whether the target is already mapped - for(m = 0; m < map[u].destinations; m++){ - if(map[u].to[m] == to){ + for(m = 0; m < routing.map[bucket][u].destinations; m++){ + if(routing.map[bucket][u].to[m] == to){ return 0; } } - map[u].to = realloc(map[u].to, (map[u].destinations + 1) * sizeof(channel*)); - if(!map[u].to){ + //add a mapping target + routing.map[bucket][u].to = realloc(routing.map[bucket][u].to, (routing.map[bucket][u].destinations + 1) * sizeof(channel*)); + if(!routing.map[bucket][u].to){ fprintf(stderr, "Failed to allocate memory\n"); - map[u].destinations = 0; + routing.map[bucket][u].destinations = 0; return 1; } - map[u].to[map[u].destinations] = to; - map[u].destinations++; + routing.map[bucket][u].to[routing.map[bucket][u].destinations] = to; + routing.map[bucket][u].destinations++; return 0; } -static void map_free(){ - size_t u; - for(u = 0; u < mappings; u++){ - free(map[u].to); +static void routing_cleanup(){ + size_t u, n; + + for(u = 0; u < sizeof(routing.map) / sizeof(routing.map[0]); u++){ + for(n = 0; n < routing.entries[u]; n++){ + free(routing.map[u][n].to); + } + free(routing.map[u]); + routing.map[u] = NULL; + routing.entries[u] = 0; + } + + for(u = 0; u < sizeof(routing.pool) / sizeof(routing.pool[0]); u++){ + free(routing.pool[u].channel); + free(routing.pool[u].value); + routing.pool[u].alloc = 0; } - free(map); - mappings = 0; - map = NULL; } MM_API int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ @@ -120,6 +150,7 @@ MM_API int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ //find exact match for(u = 0; u < fds; u++){ if(fd[u].fd == new_fd && fd[u].backend == b){ + fd[u].impl = impl; if(!manage){ fd[u].fd = -1; fd[u].backend = NULL; @@ -161,7 +192,6 @@ MM_API int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ static void fds_free(){ size_t u; for(u = 0; u < fds; u++){ - //TODO free impl if(fd[u].fd >= 0){ close(fd[u].fd); fd[u].fd = -1; @@ -173,56 +203,46 @@ static void fds_free(){ } MM_API int mm_channel_event(channel* c, channel_value v){ - size_t u, p; + size_t u, p, bucket = routing_hash(c); //find mapped channels - for(u = 0; u < mappings; u++){ - if(map[u].from == c){ + for(u = 0; u < routing.entries[bucket]; u++){ + if(routing.map[bucket][u].from == c){ break; } } - if(u == mappings){ + if(u == routing.entries[bucket]){ //target-only channel return 0; } //resize event structures to fit additional events - if(primary->n + map[u].destinations >= primary->alloc){ - primary->channel = realloc(primary->channel, (primary->alloc + map[u].destinations) * sizeof(channel*)); - primary->value = realloc(primary->value, (primary->alloc + map[u].destinations) * sizeof(channel_value)); + if(routing.events->n + routing.map[bucket][u].destinations >= routing.events->alloc){ + routing.events->channel = realloc(routing.events->channel, (routing.events->alloc + routing.map[bucket][u].destinations) * sizeof(channel*)); + routing.events->value = realloc(routing.events->value, (routing.events->alloc + routing.map[bucket][u].destinations) * sizeof(channel_value)); - if(!primary->channel || !primary->value){ + if(!routing.events->channel || !routing.events->value){ fprintf(stderr, "Failed to allocate memory\n"); - primary->alloc = 0; - primary->n = 0; + routing.events->alloc = 0; + routing.events->n = 0; return 1; } - primary->alloc += map[u].destinations; + routing.events->alloc += routing.map[bucket][u].destinations; } //enqueue channel events //FIXME this might lead to one channel being mentioned multiple times in an apply call - for(p = 0; p < map[u].destinations; p++){ - primary->channel[primary->n + p] = map[u].to[p]; - primary->value[primary->n + p] = v; + memcpy(routing.events->channel + routing.events->n, routing.map[bucket][u].to, routing.map[bucket][u].destinations * sizeof(channel*)); + for(p = 0; p < routing.map[bucket][u].destinations; p++){ + routing.events->value[routing.events->n + p] = v; } - primary->n += map[u].destinations; + routing.events->n += routing.map[bucket][u].destinations; return 0; } -static void event_free(){ - size_t u; - - for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ - free(event_pool[u].channel); - free(event_pool[u].value); - event_pool[u].alloc = 0; - } -} - static void version(){ printf("MIDIMonster %s\n", MIDIMONSTER_VERSION); } @@ -257,13 +277,27 @@ static fd_set fds_collect(int* max_fd){ } static int platform_initialize(){ -#ifdef _WIN32 + #ifdef _WIN32 WSADATA wsa; WORD version = MAKEWORD(2, 2); if(WSAStartup(version, &wsa)){ return 1; } -#endif + #endif + return 0; +} + +static int platform_shutdown(){ + #ifdef _WIN32 + DWORD processes; + if(GetConsoleProcessList(&processes, 1) == 1){ + fprintf(stderr, "\nMIDIMonster is the last process in this console, please press any key to exit\n"); + HANDLE input = GetStdHandle(STD_INPUT_HANDLE); + SetConsoleMode(input, 0); + FlushConsoleInputBuffer(input); + WaitForSingleObject(input, INFINITE); + } + #endif return 0; } @@ -274,22 +308,155 @@ static int args_parse(int argc, char** argv, char** cfg_file){ version(); return 1; } + else if(!strcmp(argv[u], "-i")){ + if(!argv[u + 1]){ + fprintf(stderr, "Missing instance override specification\n"); + return 1; + } + if(config_add_override(override_instance, argv[u + 1])){ + return 1; + } + u++; + } + else if(!strcmp(argv[u], "-b")){ + if(!argv[u + 1]){ + fprintf(stderr, "Missing backend override specification\n"); + return 1; + } + if(config_add_override(override_backend, argv[u + 1])){ + return 1; + } + u++; + } + else{ + //if nothing else matches, it's probably the configuration file + *cfg_file = argv[u]; + } + } - //if nothing else matches, it's probably the configuration file - *cfg_file = argv[u]; + return 0; +} + +static int core_process(size_t nfds, managed_fd* signaled_fds){ + event_collection* secondary = NULL; + size_t u; + + //run backend processing, collect events + DBGPF("%lu backend FDs signaled\n", nfds); + if(backends_handle(nfds, signaled_fds)){ + return 1; + } + + while(routing.events->n){ + //swap primary and secondary event collectors + DBGPF("Swapping event collectors, %lu events in primary\n", routing.events->n); + for(u = 0; u < sizeof(routing.pool) / sizeof(routing.pool[0]); u++){ + if(routing.events != routing.pool + u){ + secondary = routing.events; + routing.events = routing.pool + u; + break; + } + } + + //push collected events to target backends + if(secondary->n && backends_notify(secondary->n, secondary->channel, secondary->value)){ + fprintf(stderr, "Backends failed to handle output\n"); + return 1; + } + + //reset the event count + secondary->n = 0; } return 0; } -int main(int argc, char** argv){ +static int core_loop(){ fd_set all_fds, read_fds; - event_collection* secondary = NULL; - struct timeval tv; - size_t u, n; managed_fd* signaled_fds = NULL; - int rv = EXIT_FAILURE, error, maxfd = -1; + struct timeval tv; + int error, maxfd = -1; + size_t n, u; + #ifdef _WIN32 + char* error_message = NULL; + #else + struct timespec ts; + #endif + + FD_ZERO(&all_fds); + + //process events + while(!shutdown_requested){ + //rebuild fd set if necessary + if(fd_set_dirty || !signaled_fds){ + all_fds = fds_collect(&maxfd); + signaled_fds = realloc(signaled_fds, fds * sizeof(managed_fd)); + if(!signaled_fds){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + fd_set_dirty = 0; + } + + //wait for & translate events + read_fds = all_fds; + tv = backend_timeout(); + + //check whether there are any fds active, windows does not like select() without descriptors + if(maxfd >= 0){ + error = select(maxfd + 1, &read_fds, NULL, NULL, &tv); + if(error < 0){ + #ifndef _WIN32 + fprintf(stderr, "select failed: %s\n", strerror(errno)); + #else + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error_message, 0, NULL); + fprintf(stderr, "select failed: %s\n", error_message); + LocalFree(error_message); + error_message = NULL; + #endif + free(signaled_fds); + return 1; + } + } + else{ + DBGPF("No descriptors, sleeping for %zu msec", tv.tv_sec * 1000 + tv.tv_usec / 1000); + #ifdef _WIN32 + Sleep(tv.tv_sec * 1000 + tv.tv_usec / 1000); + #else + ts.tv_sec = tv.tv_sec; + ts.tv_nsec = tv.tv_usec * 1000; + nanosleep(&ts, NULL); + #endif + } + + //update this iteration's timestamp + update_timestamp(); + + //find all signaled fds + n = 0; + for(u = 0; u < fds; u++){ + if(fd[u].fd >= 0 && FD_ISSET(fd[u].fd, &read_fds)){ + signaled_fds[n] = fd[u]; + n++; + } + } + + //fetch and process events + if(core_process(n, signaled_fds)){ + free(signaled_fds); + return 1; + } + } + + free(signaled_fds); + return 0; +} + +int main(int argc, char** argv){ + int rv = EXIT_FAILURE; char* cfg_file = DEFAULT_CFG; + size_t u, n = 0, max = 0; //parse commandline arguments if(args_parse(argc, argv, &cfg_file)){ @@ -301,7 +468,6 @@ int main(int argc, char** argv){ return EXIT_FAILURE; } - FD_ZERO(&all_fds); //initialize backends if(plugins_load(PLUGINS)){ fprintf(stderr, "Failed to initialize a backend\n"); @@ -312,14 +478,13 @@ int main(int argc, char** argv){ if(config_read(cfg_file)){ fprintf(stderr, "Failed to read configuration file %s\n", cfg_file); backends_stop(); - channels_free(); - instances_free(); - map_free(); + routing_cleanup(); fds_free(); plugins_close(); - return usage(argv[0]); + config_free(); + return (usage(argv[0]) | platform_shutdown()); } - + //load an initial timestamp update_timestamp(); @@ -330,79 +495,31 @@ int main(int argc, char** argv){ signal(SIGINT, signal_handler); - //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(); - error = select(maxfd + 1, &read_fds, NULL, NULL, &tv); - if(error < 0){ - fprintf(stderr, "select failed: %s\n", strerror(errno)); - break; - } - - //find all signaled fds - n = 0; - for(u = 0; u < fds; u++){ - if(fd[u].fd >= 0 && FD_ISSET(fd[u].fd, &read_fds)){ - signaled_fds[n] = fd[u]; - n++; - } - } - - //update this iteration's timestamp - update_timestamp(); - - //run backend processing, collect events - DBGPF("%lu backend FDs signaled\n", n); - if(backends_handle(n, signaled_fds)){ - goto bail; - } - - while(primary->n){ - //swap primary and secondary event collectors - DBGPF("Swapping event collectors, %lu events in primary\n", primary->n); - for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ - if(primary != event_pool + u){ - secondary = primary; - primary = event_pool + u; - break; - } - } + //count and report mappings + for(u = 0; u < sizeof(routing.map) / sizeof(routing.map[0]); u++){ + n += routing.entries[u]; + max = max(max, routing.entries[u]); + } + LOGPF("Routing %" PRIsize_t " sources, largest bucket has %" PRIsize_t " entries", + n, max); - //push collected events to target backends - if(secondary->n && backends_notify(secondary->n, secondary->channel, secondary->value)){ - fprintf(stderr, "Backends failed to handle output\n"); - goto bail; - } + if(!fds){ + fprintf(stderr, "No descriptors registered for multiplexing\n"); + } - //reset the event count - secondary->n = 0; - } + //run the core loop + if(!core_loop()){ + rv = EXIT_SUCCESS; } - rv = EXIT_SUCCESS; bail: //free all data - free(signaled_fds); backends_stop(); - channels_free(); - instances_free(); - map_free(); + routing_cleanup(); fds_free(); - event_free(); plugins_close(); + config_free(); + platform_shutdown(); return rv; } diff --git a/midimonster.h b/midimonster.h index 5844bb9..75eb30a 100644 --- a/midimonster.h +++ b/midimonster.h @@ -7,7 +7,7 @@ /* Core version unless set by the build process */ #ifndef MIDIMONSTER_VERSION - #define MIDIMONSTER_VERSION "v0.3-dist" + #define MIDIMONSTER_VERSION "v0.5-dist" #endif /* Set backend name if unset */ @@ -93,8 +93,9 @@ struct _managed_fd; * Parse backend-global configuration options from the user-supplied * configuration file. Returning a non-zero value fails config parsing. * * mmbackend_instance - * Allocate space for a backend instance. Returning NULL signals an out-of-memory - * condition and terminates the program. + * Allocate the backend-specific data parts of the supplied instance + * structure. Returning non-zero signals an error condition and + * terminates the program. * * mmbackend_configure_instance * Parse instance configuration from the user-supplied configuration * file. Returning a non-zero value fails config parsing. @@ -135,7 +136,7 @@ struct _managed_fd; * 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 int (*mmbackend_create_instance)(struct _backend_instance* inst); 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); @@ -189,33 +190,10 @@ 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() + * Backends may either manage their own channel registry or use the global + * channel store via the mm_channel() API */ typedef struct _backend_channel { instance* instance; @@ -224,7 +202,7 @@ typedef struct _backend_channel { } channel; /* - * File descriptor management structure + * File descriptor structure passed for backend handling * Register for the core event loop using mm_manage_fd() */ typedef struct _managed_fd { @@ -233,68 +211,48 @@ typedef struct _managed_fd { void* impl; } managed_fd; -/* Internal channel mapping structure - Core use only */ -typedef struct /*_mm_channel_mapping*/ { - channel* from; - size_t destinations; - channel** to; -} channel_mapping; - /* * Register a new backend. */ MM_API int mm_backend_register(backend b); /* - * Provides a pointer to a newly (zero-)allocated instance. - * All instance pointers need to be allocated via this API - * in order to be assignable from the configuration parser. - * This API should be called from the mmbackend_create_instance - * call of your backend. - * - * Instances returned from this call are freed by midimonster. - * The contents of the impl members should be freed in the - * mmbackend_shutdown procedure of the backend, eg. by querying - * all instances for the backend. - */ -MM_API instance* mm_instance(); - -/* * Finds an instance matching the specified backend and identifier. - * Since setting an identifier for an instance is optional, - * this may not work depending on the backend. - * Instance identifiers may for example be set in the backends - * mmbackend_start call. + * Since setting an identifier for an instance is optional, this may not work + * depending on the backend. Instance identifiers may for example be set in the + * backends mmbackend_start call. */ MM_API instance* mm_instance_find(char* backend, uint64_t ident); /* - * Provides a pointer to a channel structure, pre-filled with - * the provided instance reference and identifier. - * The `create` parameter is a boolean flag indicating whether - * a channel matching the `ident` parameter should be created if - * none exists. If the instance already registered a channel - * matching `ident`, a pointer to it is returned. - * This API is just a convenience function. The array of channels is - * only used for mapping internally, creating and managing your own - * channel store is possible. + * Provides a pointer to a channel structure, pre-filled with the provided + * instance reference and identifier. + * The `create` parameter is a boolean flag indicating whether a channel + * matching the `ident` parameter should be created in the global channel store + * if none exists yet. If the instance already registered a channel matching + * `ident`, a pointer to the existing channel is returned. + * This API is just a convenience function. Creating and managing a + * backend-internal channel store is possible (and encouraged for performance + * reasons). When returning pointers from a backend-local channel store, the + * returned pointers must stay valid over the lifetime of the instance and + * provide valid `instance` members, as they are used for callbacks. * For each channel with a non-NULL `impl` field registered using * this function, the backend will receive a call to its channel_free - * function. + * function (if it exists). */ MM_API channel* mm_channel(instance* i, uint64_t ident, uint8_t create); -//TODO channel* mm_channel_find() /* - * Register (manage = 1) or unregister (manage = 0) a file descriptor - * to be selected on. The backend will be notified when the descriptor - * becomes ready to read via its registered mmbackend_process_fd call. + * Register (manage = 1) or unregister (manage = 0) a file descriptor to be + * selected on. The backend will be notified when the descriptor becomes ready + * to read via its registered mmbackend_process_fd call. The `impl` argument + * will be provided within the corresponding managed_fd structure upon callback. */ MM_API int mm_manage_fd(int fd, char* backend, int manage, void* impl); /* - * Notifies the core of a channel event. Called by backends to - * inject events gathered from their backing implementation. + * Notifies the core of a channel event. Called by backends to inject events + * gathered from their backing implementation. */ MM_API int mm_channel_event(channel* c, channel_value v); @@ -306,14 +264,14 @@ MM_API int mm_backend_instances(char* backend, size_t* n, instance*** i); /* * Query an internal timestamp, which is updated every core iteration. - * This timestamp should not be used as a performance counter, but can be - * used for timeouting. Resolution is milliseconds. + * This timestamp should not be used as a performance counter, but can be used + * for timeouting. Resolution is milliseconds. */ MM_API uint64_t mm_timestamp(); /* - * Create a channel-to-channel mapping. This API should not - * be used by backends. It is only exported for core modules. + * Create a channel-to-channel mapping. This API should not be used by backends. + * It is only exported for core modules. */ int mm_map_channel(channel* from, channel* to); #endif diff --git a/midimonster.ico b/midimonster.ico Binary files differnew file mode 100644 index 0000000..9391160 --- /dev/null +++ b/midimonster.ico diff --git a/midimonster.rc b/midimonster.rc new file mode 100644 index 0000000..45a88aa --- /dev/null +++ b/midimonster.rc @@ -0,0 +1,22 @@ +#include "midimonster.h" + +0 ICON "midimonster.ico" +1 VERSIONINFO +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", "control8r" + VALUE "FileDescription", "MIDIMonster" + VALUE "InternalName", "MIDIMonster Core (Windows Build)" + VALUE "FileVersion", MIDIMONSTER_VERSION + VALUE "OriginalFilename", "midimonster.exe" + VALUE "ProductName", "MIDIMonster" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END |