diff options
49 files changed, 2575 insertions, 603 deletions
diff --git a/.travis-ci.sh b/.travis-ci.sh index 1475dea..8008026 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -5,110 +5,99 @@ set -e -COVERITY_SCAN_BUILD_URL="https://scan.coverity.com/scripts/travisci_build_coverity_scan.sh" - SPELLINGBLACKLIST=$(cat <<-BLACKLIST - -wholename "./.git/*" +-wholename "./.git/*" BLACKLIST ) if [[ $TASK = 'spellintian' ]]; then - # run spellintian only if it is the requested task, ignoring duplicate words - spellingfiles=$(eval "find ./ -type f -and ! \( \ - $SPELLINGBLACKLIST \ - \) | xargs") - # count the number of spellintian errors, ignoring duplicate words - spellingerrors=$(zrun spellintian $spellingfiles 2>&1 | grep -v "\(duplicate word\)" | wc -l) - if [[ $spellingerrors -ne 0 ]]; then - # print the output for info - zrun spellintian $spellingfiles | grep -v "\(duplicate word\)" - echo "Found $spellingerrors spelling errors via spellintian, ignoring duplicates" - exit 1; - else - echo "Found $spellingerrors spelling errors via spellintian, ignoring duplicates" - fi; + # 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; + # 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; + # 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 = 'sanitize' ]]; then - # Run sanitized compile - travis_fold start "make_sanitize" - make sanitize; - travis_fold end "make_sanitize" + # 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" - travis_fold start "deploy_windows" - if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then - 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 - fi - travis_fold end "deploy_windows" + travis_fold start "make_windows" + make windows; + 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" - travis_fold start "deploy_unix" - if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then - 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 - fi - travis_fold end "deploy_unix" + # Otherwise compile as normal + travis_fold start "make" + make full; + 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 21c2a40..b9b6969 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ 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 @@ -22,7 +23,9 @@ addons: - libola-dev - libjack-jackd2-dev - liblua5.3-dev + - python3-dev - libssl-dev + - lintian packages: &core_build_gpp_latest - *core_build - gcc-8 @@ -34,47 +37,27 @@ addons: - *core_build - mingw-w64 -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' @@ -83,33 +66,16 @@ 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 - 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 + dist: bionic env: TASK='spellintian' addons: apt: @@ -117,7 +83,7 @@ matrix: - *core_build - moreutils - os: linux - dist: xenial + dist: bionic env: TASK='spellintian-duplicates' addons: apt: @@ -125,23 +91,34 @@ matrix: - *core_build - moreutils - os: linux - dist: xenial + dist: bionic env: TASK='codespell' addons: apt: packages: - *core_build - moreutils - allow_failures: - - 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' + env: + - TASK='compile' + - os: osx + osx_image: xcode10.2 + compiler: clang + env: + - TASK='sanitize' + allow_failures: - os: linux - dist: xenial + dist: bionic env: TASK='spellintian-duplicates' - os: linux - dist: xenial + dist: bionic env: TASK='codespell' env: @@ -150,21 +127,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 @@ -182,14 +144,14 @@ before_install: - git pull --tags - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi # 'brew install' sometimes returns non-zero for some arcane reason. Executing 'true' resets the exit code and allows Travis to continue building... - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install ccache ola lua openssl jack; true; fi +# 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 @@ -197,15 +159,13 @@ 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 - -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 +# 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 deploy: provider: releases file_glob: true - api_key: $GITHUB_TOKEN + token: $GITHUB_TOKEN file: ./deployment/* skip_cleanup: true draft: 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 @@ -18,6 +18,7 @@ midimonster: LDLIBS = -ldl 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 @@ -54,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 @@ -79,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,84 @@ 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 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 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 +240,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 @@ -54,7 +54,7 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ if(c[p]->instance == instances[u]){ xval = v[n]; xchnl = c[n]; - + v[n] = v[p]; c[n] = c[p]; @@ -105,7 +105,7 @@ MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){ return channels[nchannels++]; } -MM_API instance* mm_instance(){ +instance* mm_instance(){ instance** new_inst = realloc(instances, (ninstances + 1) * sizeof(instance*)); if(!new_inst){ //TODO free @@ -274,7 +274,7 @@ int backends_start(){ if(p == ninstances){ 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); @@ -10,10 +10,10 @@ int backends_start(); int backends_stop(); void instances_free(); void channels_free(); +instance* mm_instance(); /* 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 656e6b6..e31ff24 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 -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so +WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so 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) @@ -59,6 +63,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..9fac332 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,17 @@ 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; - } - - data = calloc(1, sizeof(artnet_instance_data)); +static int artnet_instance(instance* inst){ + artnet_instance_data* data = calloc(1, sizeof(artnet_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->net = default_net; inst->impl = data; - return inst; + return 0; } static int artnet_configure_instance(instance* inst, char* option, char* value){ @@ -217,13 +219,15 @@ 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){ + uint32_t frame_delta = 0; size_t u, mark = 0; artnet_instance_data* data = (artnet_instance_data*) inst->impl; @@ -232,7 +236,6 @@ 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])){ uint32_t val = v[u].normalised * ((double) 0xFFFF); @@ -254,6 +257,22 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) } 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_KEEPALIVE_INTERVAL - frame_delta)){ + next_frame = (ARTNET_KEEPALIVE_INTERVAL - frame_delta); + } + return 0; + } return artnet_transmit(inst); } @@ -323,6 +342,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 +351,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 +443,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 +479,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..d83999d 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 15 +#define ARTNET_SYNTHESIZE_MARGIN 10 #define MAP_COARSE 0x0200 #define MAP_FINE 0x0400 @@ -52,11 +57,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..7e1ecff 100644 --- a/backends/artnet.md +++ b/backends/artnet.md @@ -36,6 +36,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..e7ba9f9 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -9,6 +9,7 @@ #endif #define LUA_REGISTRY_KEY "_midimonster_lua_instance" +#define LUA_REGISTRY_CURRENT_CHANNEL "_midimonster_lua_channel" static size_t timers = 0; static lua_timer* timer = NULL; @@ -63,6 +64,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 +76,7 @@ static int lua_update_timerfd(){ size_t n = 0; #ifdef MMBACKEND_LUA_TIMERFD struct itimerspec timer_config = { - 0 + {0} }; #endif @@ -84,6 +86,7 @@ static int lua_update_timerfd(){ interval = timer[n].interval; } } + DBGPF("Recalculating timers, minimum is %" PRIu64, interval); //calculate gcd of all timers if any are active if(interval){ @@ -110,11 +113,13 @@ 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; @@ -189,6 +194,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 +205,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 @@ -256,7 +264,7 @@ 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]); + lua_pushnumber(interpreter, (input) ? data->input[n] : data->output[n]); return 1; } } @@ -273,6 +281,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; @@ -294,16 +313,11 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ 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 +325,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 +334,8 @@ 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); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -327,7 +343,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){ @@ -374,6 +390,11 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ data->input[c[n]->ident] = v[n].normalised; //call lua channel handlers if present if(data->reference[c[n]->ident] != LUA_NOREF){ + //push the channel name + lua_pushstring(data->interpreter, LUA_REGISTRY_CURRENT_CHANNEL); + lua_pushstring(data->interpreter, data->channel_name[c[n]->ident]); + lua_settable(data->interpreter, LUA_REGISTRYINDEX); + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); lua_pushnumber(data->interpreter, v[n].normalised); if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ @@ -382,6 +403,11 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ } } } + + //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 +427,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,6 +445,7 @@ 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); } } } @@ -440,6 +464,8 @@ static int lua_start(size_t n, instance** inst){ if(strcmp(data->channel_name[p], "output") && strcmp(data->channel_name[p], "input_value") && strcmp(data->channel_name[p], "output_value") + && strcmp(data->channel_name[p], "input_channel") + && strcmp(data->channel_name[p], "timestamp") && strcmp(data->channel_name[p], "interval")){ lua_getglobal(data->interpreter, data->channel_name[p]); data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); @@ -456,6 +482,8 @@ static int lua_start(size_t n, instance** inst){ if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ return 1; } + #else + last_timestamp = mm_timestamp(); #endif return 0; } diff --git a/backends/lua.h b/backends/lua.h index 75f03c4..ebe2046 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); diff --git a/backends/lua.md b/backends/lua.md index f38e189..db4cf39 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -1,9 +1,9 @@ ### 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. @@ -13,10 +13,11 @@ The following functions are provided within the Lua interpreter for interaction | 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 | Example script: ``` @@ -45,7 +46,7 @@ The `lua` backend does not take any global configuration. |---------------|-----------------------|-----------------------|-----------------------| | `script` | `script.lua` | none | Lua source file (relative to configuration file)| -A single instance may have multiple `source` options specified, which will all be read cumulatively. +A single instance may have multiple `script` options specified, which will all be read cumulatively. #### Channel specification @@ -58,9 +59,20 @@ 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 any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`, +`timestamp`) as an input channel name to a Lua instance will not call any handler functions. Using these names as arguments to the output and value interface functions works as intended. 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 f81ab46..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)){ 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..f73ebb4 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -69,19 +69,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){ 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/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..168e077 --- /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..70c2548 --- /dev/null +++ b/backends/python.c @@ -0,0 +1,699 @@ +#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){ + 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* chan = 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); + + for(u = 0; u < data->channels; u++){ + if(!strcmp(data->channel[u].name, channel_name)){ + DBGPF("Setting channel %s.%s to %f", inst->name, channel_name, val.normalised); + chan = mm_channel(inst, u, 0); + //this should never happen + if(!chan){ + LOGPF("Failed to fetch parsed channel %s.%s", inst->name, channel_name); + break; + } + data->channel[u].out = val.normalised; + mm_channel_event(chan, val); + break; + } + } + + if(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; + } + + 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 int python_start(size_t n, instance** inst){ + python_instance_data* data = NULL; + PyObject* module = NULL; + size_t u, p; + char* module_name = NULL, *channel_name = NULL; + + //resolve channel references to handler functions + for(u = 0; u < n; u++){ + data = (python_instance_data*) inst[u]->impl; + + //switch to interpreter + PyEval_RestoreThread(data->interpreter); + for(p = 0; p < data->channels; p++){ + module = PyImport_AddModule("__main__"); + channel_name = data->channel[p].name; + module_name = strchr(channel_name, '.'); + if(module_name){ + *module_name = 0; + //returns borrowed reference + module = PyImport_AddModule(channel_name); + + if(!module){ + LOGPF("Module %s for qualified channel %s.%s is not loaded on instance %s", channel_name, channel_name, module_name + 1, inst[u]->name); + return 1; + } + + *module_name = '.'; + channel_name = module_name + 1; + } + + //returns new reference + data->channel[p].handler = PyObject_GetAttrString(module, channel_name); + } + + //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); + //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); + } + + 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..8ca12f9 --- /dev/null +++ b/backends/python.h @@ -0,0 +1,44 @@ +#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; +} 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; +} python_instance_data; diff --git a/backends/python.md b/backends/python.md new file mode 100644 index 0000000..f06e504 --- /dev/null +++ b/backends/python.md @@ -0,0 +1,99 @@ +### 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. + +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 | + +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..79ffb46 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,23 @@ 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){ inst->impl = calloc(1, sizeof(sacn_instance_data)); if(!inst->impl){ LOG("Failed to allocate memory"); - return NULL; + return 1; } - return inst; + return 0; } static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ @@ -215,7 +223,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); @@ -294,10 +302,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 +314,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 +348,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_KEEPALIVE_INTERVAL - frame_delta)){ + global_cfg.next_frame = (SACN_KEEPALIVE_INTERVAL - frame_delta); + } + return 0; + } + } sacn_transmit(inst); } @@ -473,6 +502,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 +512,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 +522,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 +604,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 +636,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 +660,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 +680,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..4642e59 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 15 +#define SACN_SYNTHESIZE_MARGIN 10 #define SACN_DISCOVERY_TIMEOUT 9000 #define SACN_PDU_MAGIC "ASC-E1.17\0\0\0" @@ -35,6 +40,7 @@ typedef struct /*_sacn_universe_model*/ { 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 +60,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..ad9b02d 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){ @@ -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); @@ -396,7 +391,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 +424,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); 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(); + 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/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..b8594b4 100644 --- a/midimonster.c +++ b/midimonster.c @@ -14,6 +14,7 @@ #include "backend.h" #include "plugin.h" +/* Core-internal structures */ typedef struct /*_event_collection*/ { size_t alloc; size_t n; @@ -21,6 +22,12 @@ typedef struct /*_event_collection*/ { channel_value* value; } event_collection; +typedef struct /*_mm_channel_mapping*/ { + channel* from; + size_t destinations; + channel** to; +} channel_mapping; + static size_t mappings = 0; static channel_mapping* map = NULL; static size_t fds = 0; @@ -120,6 +127,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; @@ -257,13 +265,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,9 +296,30 @@ static int args_parse(int argc, char** argv, char** cfg_file){ version(); return 1; } - - //if nothing else matches, it's probably the configuration file - *cfg_file = argv[u]; + 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]; + } } return 0; @@ -290,6 +333,9 @@ int main(int argc, char** argv){ managed_fd* signaled_fds = NULL; int rv = EXIT_FAILURE, error, maxfd = -1; char* cfg_file = DEFAULT_CFG; + #ifdef _WIN32 + char* error_message = NULL; + #endif //parse commandline arguments if(args_parse(argc, argv, &cfg_file)){ @@ -317,9 +363,10 @@ int main(int argc, char** argv){ map_free(); fds_free(); plugins_close(); - return usage(argv[0]); + config_free(); + return (usage(argv[0]) | platform_shutdown()); } - + //load an initial timestamp update_timestamp(); @@ -348,7 +395,15 @@ int main(int argc, char** argv){ tv = backend_timeout(); error = select(maxfd + 1, &read_fds, NULL, NULL, &tv); if(error < 0){ + #ifndef _WIN32 fprintf(stderr, "select failed: %s\n", strerror(errno)); + #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 break; } @@ -403,6 +458,8 @@ bail: fds_free(); event_free(); plugins_close(); + config_free(); + platform_shutdown(); return rv; } diff --git a/midimonster.h b/midimonster.h index 5844bb9..2c29956 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.4-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,29 +190,6 @@ 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 @@ -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,33 +211,12 @@ 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. @@ -283,7 +240,6 @@ MM_API instance* mm_instance_find(char* backend, uint64_t ident); * function. */ 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 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 |